This series of tutorials is inspired by two things:
- LatinoWare 2008, where I presented this very app as an introduction to PyQt development.
- A lack of (in my very humble opinion) PyQt tutorials that show the way I prefer to develop applications.
The second item may sound a bit belligerent, but that’s not the case. I am not saying the other tutorials are wrong, or bad, I just say they don’t work the way I like to work.
I don’t believe in teaching something and later saying “now I will shw you how it’s really done”. I don’t believe in toy examples. I believe that you are smart enough to only learn things once, and learning the true thing the first time.
So, in this series, I will be developing a small TODO application using the tools and procedures I use in my actual development, except for the Eric IDE. That is because IDEs are personal preferences and for a project fo this scope really doesn’t add much.
One other thing I will not add is unit testing. While very important, I think it would distract from actually doing. If that’s a problem, it can be added in a later version of the tutorial.
You must have installed the following programs:
- Python: I am using 2.6, I expect 2.5 or even 2.4 will work, but I am not testing them.
- Elixir: This is needed by the backend. It requires SQLAlchemy and we will be using SQLite as our database. If you install Elixir everything else should be installed automatically.
- PyQt: I will be using version 4.4
- Your text editor of choice
This tutorial doesn’t assume knowledge of Elixir, PyQt or databases, but does assume a working knowledge of Python. If you don’t know python yet, this is not the right tutorial for you yet.
You can get the full code for this session here: Sources (click the “Download” button).
Since this tutorial is completely hosted in GitHub you are free to contribute improvements, modifications, even whole new sessions or features!
Session 1: The basics
Since we are developing a TODO application, we need a backend that handles the storage, retrieval and general managing of TODO tasks.
To do that the simplest possible way, I will do it using Elixir, “A declarative layer over the SQLAlchemy Object-Relational Mapper”.
If that sounded very scary, don’t worry. What that means is “a way to create objects that are automatically stored in a database”.
Here is the code, with comments, for our backend, called todo.py. Hopefully, we will not have to look at it again until much later in the tutorial!
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 recommend using designer to create your graphical interfaces. Yes, some people complain about interface designers. I say you should spend your time writing code for the parts where there are no good tools instead.
And here is the Qt Designer file for it: window.ui. Don’t worry about all that XML, just open it on your designer ;-)
This is how it looks in designer:
What you see is a “Main Window”. This kind of window lets you have a menu, toolbars, status bars, and is the typical window for a standard application.
The “Type Here” at the top is because the menu is still empty, and it’s “inviting” you to add something to it.
The big square thing with “Task” “Date” and “Tags” is a widget called QTreeView, which is handy to display items with icons, several columns, and maybe a hierarchical structure (A tree, thus the name). We will use it to display our task list.
You can see how this window looks by using “Form” -> “Preview” in designer. THis is what you’ll get:
You can try resizing the window, and this widget will use all available space and resize alongside the window. That’s important: windows that can’t handle resizing properly look unprofessional and are not adequate.
In Qt, this is done using layouts. In this particular case, since we have only one widget, what we do is click on the form’s background and select “Layout” -> “Layout Horizontally” (Vertically would have had the exact same effect here).
When we do a configuration dialog, we will learn more about layouts.
Now, feel free to play with designer and this form. You could try changing the layout, add new things, change properties of the widgets, experiment at will, learning designer is worth the effort!
Using our Main Window
We are now going to make this window we created part of a real program, so we can then start making it work.
First we need to compile our .ui file into python code. You can do this with this command:
pyuic4 window.ui -o windowUi.py
Now let’s look at main.py, our application’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 specific to our TODO application. Whatever was in that .ui file would work just fine with this!
The only interesting part is the Main class. That class uses the compiled ui file and is where we will put our application’s user interface logic. You never edit the .ui file or the generated python file manually!
Let me put that in these terms: IF YOU EDIT THE UI FILE (WITHOUT USING DESIGNER) OR THE GENERATED PYTHON FILE YOU ARE DOING IT WRONG! YOU FAIL! EPIC FAIL!. I hope that got across, because there is at least one tutorial 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 application run. It will do nothing interesting, because we need to attach the backend to our user interface, but that’s session 2.