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­sent­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!

 1  # -*- coding: utf-8 -*-
 2 
 3  """A simple backend for a TODO app, using Elixir"""
 4 
 5  import os
 6  from elixir import *
 7 
 8  dbdir=os.path.join(os.path.expanduser("~"),".pyqtodo")
 9  dbfile=os.path.join(dbdir,"tasks.sqlite")
10 
11  class Task(Entity):
12      """
13      A task for your TODO list.
14      """
15      using_options(tablename='tasks')
16      text = Field(Unicode,required=True)
17      date = Field(DateTime,default=None,required=False)
18      done = Field(Boolean,default=False,required=True)
19      tags  = ManyToMany("Tag")
20 
21      def __repr__(self):
22          return "Task: "+self.text
23 
24 
25  class Tag(Entity):
26      """
27      A tag we can apply to a task.
28      """
29      using_options(tablename='tags')
30      name = Field(Unicode,required=True)
31      tasks = ManyToMany("Task")
32 
33      def __repr__(self):
34          return "Tag: "+self.name
35 
36 
37  saveData=None
38 
39  def initDB():
40      if not os.path.isdir(dbdir):
41          os.mkdir(dbdir)
42      metadata.bind = "sqlite:///%s"%dbfile
43      setup_all()
44      if not os.path.exists(dbfile):
45          create_all()
46 
47      # This is so Elixir 0.5.x and 0.6.x work
48      # Yes, it's kinda ugly, but needed for Debian
49      # and Ubuntu and other distros.
50 
51      global saveData
52      import elixir
53      if elixir.__version__ < "0.6":
54          saveData=session.flush
55      else:
56          saveData=session.commit
57 
58 
59 
60  def main():
61 
62      # Initialize database
63      initDB()
64 
65      # Create two tags
66      green=Tag(name=u"green")
67      red=Tag(name=u"red")
68 
69      #Create a few tags and tag them
70      tarea1=Task(text=u"Buy tomatos",tags=[red])
71      tarea2=Task(text=u"Buy chili",tags=[red])
72      tarea3=Task(text=u"Buy lettuce",tags=[green])
73      tarea4=Task(text=u"Buy strawberries",tags=[red,green])
74      saveData()
75 
76      print "Green Tasks:"
77      print green.tasks
78      print
79      print "Red Tasks:"
80      print red.tasks
81      print
82      print "Tasks with l:"
83      print [(t.id,t.text,t.done) for t in Task.query.filter(Task.text.like(ur'%l%')).all()]
84 
85  if __name__ == "__main__":
86      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:

 1  # -*- coding: utf-8 -*-
 2 
 3  """The user interface for our app"""
 4 
 5  import os,sys
 6 
 7  # Import Qt modules
 8  from PyQt4 import QtCore,QtGui
 9 
10  # Import the compiled UI module
11  from windowUi import Ui_MainWindow
12 
13  # Create a class for our main window
14  class Main(QtGui.QMainWindow):
15      def __init__(self):
16          QtGui.QMainWindow.__init__(self)
17 
18          # This is always the same
19          self.ui=Ui_MainWindow()
20          self.ui.setupUi(self)
21 
22  def main():
23      # Again, this is boilerplate, it's going to be the same on
24      # almost every app you write
25      app = QtGui.QApplication(sys.argv)
26      window=Main()
27      window.show()
28      # It's exec_ because exec is a reserved word in Python
29      sys.exit(app.exec_())
30 
31 
32  if __name__ == "__main__":
33      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.

Comments

Comments powered by Disqus
Contents © 2000-2013 Roberto Alsina
Share