Skip to main content

Ralsina.Me — Roberto Alsina's website

PyQt by Example (Session 1)

Introduction

This se­ries of tu­to­ri­als is in­spired by two things:

  • Lati­noWare 2008, where I pre­sen­t­ed this very app as an in­­tro­­duc­­tion to PyQt de­vel­op­­men­t.

  • A lack of (in my very hum­ble opin­ion) PyQt tu­­to­ri­als that show the way I pre­fer to de­vel­op ap­­pli­­ca­­tion­s.

The sec­ond item may sound a bit bel­liger­en­t, but that's not the case. I am not say­ing the oth­er tu­to­ri­als are wrong, or bad, I just say they don't work the way I like to work.

I don't be­lieve in teach­ing some­thing and lat­er say­ing "now I will shw you how it's re­al­ly done". I don't be­lieve in toy ex­am­ples. I be­lieve that you are smart enough to on­ly learn things on­ce, and learn­ing the true thing the first time.

So, in this se­ries, I will be de­vel­op­ing a small TO­DO ap­pli­ca­tion us­ing the tools and pro­ce­dures I use in my ac­tu­al de­vel­op­men­t, ex­cept for the Er­ic IDE. That is be­cause IDEs are per­son­al pref­er­ences and for a project fo this scope re­al­ly does­n't add much.

One oth­er thing I will not add is unit test­ing. While very im­por­tan­t, I think it would dis­tract from ac­tu­al­ly do­ing. If that's a prob­lem, it can be added in a lat­er ver­sion of the tu­to­ri­al.

Requirements

You must have in­stalled the fol­low­ing pro­gram­s:

  • Python: I am us­ing 2.6, I ex­pect 2.5 or even 2.4 will work, but I am not test­ing them.

  • Elixir: This is need­ed by the back­­end. It re­quires SQLAlche­my and we will be us­ing SQLite as our data­base. If you in­­stall Elixir ev­ery­thing else should be in­­stalled au­­to­­mat­i­­cal­­ly.

  • PyQt: I will be us­ing ver­­sion 4.4

  • Your text ed­i­­tor of choice

This tu­to­ri­al does­n't as­sume knowl­edge of Elixir, PyQt or databas­es, but does as­sume a work­ing knowl­edge of Python. If you don't know python yet, this is not the right tu­to­ri­al for you yet.

You can get the full code for this ses­sion here: Sources (click the "Down­load" but­ton).

Since this tu­to­ri­al is com­plete­ly host­ed in GitHub you are free to con­trib­ute im­prove­ments, mod­i­fi­ca­tion­s, even whole new ses­sions or fea­tures!

Session 1: The basics

The backend

The most re­cent ver­sion of this ses­sion (in RST for­mat) is al­ways avail­able at GitHub's mas­ter tree as tut1.txt

Since we are de­vel­op­ing a TO­DO ap­pli­ca­tion, we need a back­end that han­dles the stor­age, re­trieval and gen­er­al man­ag­ing of TO­DO tasks.

To do that the sim­plest pos­si­ble way, I will do it us­ing Elixir, "A declar­a­tive lay­er over the SQLAlche­my Ob­jec­t-Re­la­tion­al Map­per".

If that sound­ed very scary, don't wor­ry. What that means is "a way to cre­ate ob­jects that are au­to­mat­i­cal­ly stored in a database".

Here is the code, with com­ments, for our back­end, called to­do.py. Hope­ful­ly, we will not have to look at it again un­til much lat­er in the tu­to­ri­al!

 # -*- coding: utf-8 -*-

 """A simple backend for a TODO app, using Elixir"""

 import os
 from elixir import *

 dbdir=os.path.join(os.path.expanduser("~"),".pyqtodo")
 dbfile=os.path.join(dbdir,"tasks.sqlite")

 class Task(Entity):
     """
     A task for your TODO list.
     """
     using_options(tablename='tasks')
     text = Field(Unicode,required=True)
     date = Field(DateTime,default=None,required=False)
     done = Field(Boolean,default=False,required=True)
     tags  = ManyToMany("Tag")

     def __repr__(self):
         return "Task: "+self.text


 class Tag(Entity):
     """
     A tag we can apply to a task.
     """
     using_options(tablename='tags')
     name = Field(Unicode,required=True)
     tasks = ManyToMany("Task")

     def __repr__(self):
         return "Tag: "+self.name


 saveData=None

 def initDB():
     if not os.path.isdir(dbdir):
         os.mkdir(dbdir)
     metadata.bind = "sqlite:///%s"%dbfile
     setup_all()
     if not os.path.exists(dbfile):
         create_all()

     # This is so Elixir 0.5.x and 0.6.x work
     # Yes, it's kinda ugly, but needed for Debian
     # and Ubuntu and other distros.

     global saveData
     import elixir
     if elixir.__version__ < "0.6":
         saveData=session.flush
     else:
         saveData=session.commit



 def main():

     # Initialize database
     initDB()

     # Create two tags
     green=Tag(name=u"green")
     red=Tag(name=u"red")

     #Create a few tags and tag them
     tarea1=Task(text=u"Buy tomatos",tags=[red])
     tarea2=Task(text=u"Buy chili",tags=[red])
     tarea3=Task(text=u"Buy lettuce",tags=[green])
     tarea4=Task(text=u"Buy strawberries",tags=[red,green])
     saveData()

     print "Green Tasks:"
     print green.tasks
     print
     print "Red Tasks:"
     print red.tasks
     print
     print "Tasks with l:"
     print [(t.id,t.text,t.done) for t in Task.query.filter(Task.text.like(ur'%l%')).all()]

 if __name__ == "__main__":
     main()

The Main Window

Now, let's start with the fun part: PyQt!

I rec­om­mend us­ing de­sign­er to cre­ate your graph­i­cal in­ter­faces. Yes, some peo­ple com­plain about in­ter­face de­sign­er­s. I say you should spend your time writ­ing code for the parts where there are no good tools in­stead.

And here is the Qt De­sign­er file for it: win­dow.ui. Don't wor­ry about all that XM­L, just open it on your de­sign­er ;-)

This is how it looks in de­sign­er:

/static/tut1-window2.png

The main win­dow, in de­sign­er.

What you see is a "Main Win­dow". This kind of win­dow lets you have a menu, tool­bars, sta­tus bars, and is the typ­i­cal win­dow for a stan­dard ap­pli­ca­tion.

The "Type Here" at the top is be­cause the menu is still emp­ty, and it's "invit­ing" you to add some­thing to it.

The big square thing with "Task" "Date" and "Tags" is a wid­get called QTree­View, which is handy to dis­play items with icon­s, sev­er­al column­s, and maybe a hi­er­ar­chi­cal struc­ture (A tree, thus the name). We will use it to dis­play our task list.

You can see how this win­dow looks by us­ing "For­m" -> "Pre­view" in de­sign­er. THis is what you'll get:

/static/tut1-window1.png

The main win­dow pre­view, show­ing the task list.

You can try re­siz­ing the win­dow, and this wid­get will use all avail­able space and re­size along­side the win­dow. That's im­por­tan­t: win­dows that can't han­dle re­siz­ing prop­er­ly look un­pro­fes­sion­al and are not ad­e­quate.

In Qt, this is done us­ing lay­out­s. In this par­tic­u­lar case, since we have on­ly one wid­get, what we do is click on the for­m's back­ground and se­lect "Lay­out" -> "Lay­out Hor­i­zon­tal­ly" (Ver­ti­cal­ly would have had the ex­act same ef­fect here).

When we do a con­fig­u­ra­tion di­alog, we will learn more about lay­out­s.

Now, feel free to play with de­sign­er and this for­m. You could try chang­ing the lay­out, add new things, change prop­er­ties of the wid­get­s, ex­per­i­ment at will, learn­ing de­sign­er is worth the ef­fort!

Using our Main Window

We are now go­ing to make this win­dow we cre­at­ed part of a re­al pro­gram, so we can then start mak­ing it work.

First we need to com­pile our .ui file in­to python code. You can do this with this com­mand:

pyuic4 window.ui -o windowUi.py

Now let's look at main.py, our ap­pli­ca­tion's main file:

 # -*- coding: utf-8 -*-

 """The user interface for our app"""

 import os,sys

 # Import Qt modules
 from PyQt4 import QtCore,QtGui

 # Import the compiled UI module
 from windowUi import Ui_MainWindow

 # Create a class for our main window
 class Main(QtGui.QMainWindow):
     def __init__(self):
         QtGui.QMainWindow.__init__(self)

         # This is always the same
         self.ui=Ui_MainWindow()
         self.ui.setupUi(self)

 def main():
     # Again, this is boilerplate, it's going to be the same on
     # almost every app you write
     app = QtGui.QApplication(sys.argv)
     window=Main()
     window.show()
     # It's exec_ because exec is a reserved word in Python
     sys.exit(app.exec_())


 if __name__ == "__main__":
     main()

As you can see, this is not at all spe­cif­ic to our TO­DO ap­pli­ca­tion. What­ev­er was in that .ui file would work just fine with this!

The on­ly in­ter­est­ing part is the Main class. That class us­es the com­piled ui file and is where we will put our ap­pli­ca­tion's us­er in­ter­face log­ic. You nev­er ed­it the .ui file or the gen­er­at­ed python file man­u­al­ly!

Let me put that in these terms: IF YOU ED­IT THE UI FILE (WITH­OUT US­ING DE­SIGN­ER) OR THE GEN­ER­AT­ED PYTHON FILE YOU ARE DO­ING IT WRONG! YOU FAIL! EPIC FAIL!. I hope that got across, be­cause there is at least one tu­to­ri­al that tells you to do it. DON'T DO IT!!!,

You just put your code in this class and you will be fine.

So, if you run main.py, you will make the ap­pli­ca­tion run. It will do noth­ing in­ter­est­ing, be­cause we need to at­tach the back­end to our us­er in­ter­face, but that's ses­sion 2.

Javi / 2009-03-03 05:47:

Nice work. I'm impatient so see next part.

Note: The elixir link is wrong.

Thanks

Roberto Alsina / 2009-03-03 16:32:

Fixed, thanks!

Kel / 2009-03-04 04:52:

Very good. Keep it up!

Barbazul / 2009-03-04 12:23:

Genial!
Me canse de pedirtelo mientras estuvimos alla

BTW Felicitaciones por retomar el blog, de casualidad se me ocurrio volver por estos lares y veo que volviste y afilado!

Buen... hora de meterle a Pyqt parece

Roberto Alsina / 2009-03-04 12:53:

Tarde pero inseguro, como siempre :-)

dg / 2009-03-04 20:59:

Thanks so much for this. Just finished #1, and going to do #2 tonight.

BTW, for those using an older version of Elixir like me (I'm just using the one that comes with Ubuntu Intrepid), you need to do session.flush() instead of session.commit()

Roberto Alsina / 2009-03-04 21:47:

Hmmm.... maybe I should fix that in the code so it works for both versions.

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

Just followed session #1. Had to change the following statement to get it compiled correctly:

from windowUi import Ui_MainWindow

into:

from Ui_window import Ui_MainWindow

since the file is named Ui_window.py.

For the rest, looks very interesting, especially the "preparation" I had to do with Elixir, setuptools an SqlAlchemy. That made me learning a few more nice Python apps.

Roberto Alsina / 2009-03-08 14:51:

@Geert, the .py file will be calld whatever you say when you call pyuic. The tutorial says

pyuic4 window.ui -o windowUi.py

but if you made it work, all is well :-)

Geert Vancompernolle / 2009-03-08 14:52:

From the above remark, it's important to mention I'm using Eric4 to work on the project. Hence, the resulting UI file is called Ui_window.py (by Eric4) iso windowUi as been given in the article.

So, no issue...

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

@geert: Oh, eric! I will change it so it's compatible with eric's naming, since it's the same for me.

Alquimista / 2009-11-02 04:06:

Is possible to instal elixir in windows?

Alquimista / 2009-11-02 06:51:

I solve the problem using easy instal

patricio / 2010-01-19 23:38:

Roberto, fijate cuando hacés los test en todo.py, intercalás nombres de variable en castellano e inglés (seguramente es culpa del traductor, claro). Creás dos etiquetas (verde y rojo) y después haces green.tasks y red.tasks. Obviamente genera un error cuando lo ejecutás.

Por cierto, muy bueno el tutorial.

pablo / 2010-01-23 21:45:

Cuando vi la linea esa en donde trasnformas el archivo .ui a uno .py me sacastes de encima un gran peso. En serio yo vengo de dar mis primeros pasos. Y algo a futuro es ver esto de pyqt y mirando asi como quien no ve todo. Me encontre con que el designer(perdon si lo escribo mal.) no genera el archivo .py. En mi caso vengo de laburar con wxpython y un programa que aunque no me gusta ni mierda(wxglade) es lo que se usar por que generaba las ventanas en .py. Y la verdad que ahora me siento mas liviano. Muchisimas gracias.

Patrick / 2010-02-15 15:05:

A great article! I am currently learning PyQt and find this a great beginner article. Thanks!

Carlos / 2010-05-11 15:08:

Hey:
Sólo quiero saber una cosita, mira tengo un QMessageBox con sus standard buttons, y estos son 'OK','Cancel', 'No' Como le hago para cambiarlos a español, es facil o no? Gracias mano, ahora bien como se instala elixir. God Bless U

Roberto Alsina / 2010-05-11 15:21:

Lo de la traducción es fácil, mirá el principio de main() en este link:

http://code.google.com/p/ma...

En la linea 1433 se carga una traducción de la aplicación, en la 1437 se carga la traducción de los dialogos standard de Qt (ahí están ese OK y Cancel)

Para instalar elixir: Los detalles dependen de qué sistema operativo estés usando, pero en la pagina de Elixir hay informacion:

http://elixir.ematia.de/tra...

Bonface / 2010-08-27 12:58:

The materials are great but you know guys, you were suppose to make a tour guider manual for python. My first experience ia great and I feel is better than cpp but because am used to cpp, which has the simplest tour manual guide, it makes cpp easier though it is diffucult than python. It will be good for my fellow self learner easier, because it is not offered here. With the help of Sarah. Will continue to work on it, hoping that will keep toutch with each other.

Roberto Alsina / 2010-08-27 13:08:

Really, I was supposed to do that? Good to know!

Mugunth / 2010-10-01 01:08:

I tried with latest pyQt4 (PyQt-Py3.1-gpl-4.7.7-1) .

Need to change the line:
"from MainWindow import Ui_MainWindow" to "from windowUi import Ui_MainWindow" since the UI file name of the generated is windowUi.py

With that change, this code works fine in latet pyQt version 4.7.7

Byhanjo / 2012-04-07 02:49:

Very good. Excellent!

Murphy Randle / 2012-06-12 01:10:

Many thanks, I'm glad you have a clear, step by step tutorial like this. - Muchas gracias!