As part of the development for the forthcoming application Beaver, prototypes were made to investigate the best ways to handle things like gui design, internationalization, charting integration and so on. These (deliberately simple) examples will be made available here in case someone else can find them useful.
The topics dealt with here include:
It was decided that Beaver would be written in Python, and would use Qt for the GUI parts. Of course one can write the whole Qt stuff by hand in Python, using PyQt, but it would be easier and nicer to use the provided QtDesigner tool to make the layout and configuration easier. So here's the problem - how to combine Python, Qt and QtDesigner into a (simple) application?
Here is where somebody called "kib2" steps in, providing an excellent overview in French. This short guide, called "Utiliser PyQt4 avec QtDesigner" is made available online in pdf format (recommended) or in dark html, both including short code samples. One of those described workflows goes like this:
This approach is outlined in the following tar file, which includes an example .ui file, the generated .py file, the main program .py file and an explanatory readme.txt file.
The example used is a very simple gui using layouts, with an edit box and a button which adds this entry to a list widget. The connections between signals and slots is handled by the main program, which means that the gui file can be edited in QtDesigner and the python file regenerated, without affecting the custom event-handling code.
As with GpsPrune, it would be nice if Beaver could also be available in multiple languages, with translations provided by volunteers. But this abstraction has to be provided right from the start, to make the code language-independent. There are several ways of doing this, so first of all we look at the standard python way.
The standard Python way is using GNU gettext, which for Python has a miserable amount of indecipherable documentation. Even a simple "Hello, World" program is beyond their ability to document, and everything revolves around complex bindings and environment variables, with language files in subdirectories of subdirectories, with nothing explained. I got as far as using pygettext
to extract the texts into a .pot
file, and using poedit
to add the translations and save them to .po
and .mo
files, but making a simple Python program to actually find these files and not just spit out errors proved impossible. Maybe this is something that Python programs simply don't do.
So the next angle of attack was aimed at Qt, and they have provided a much greater amount of information, but all C++-specific. However, interestingly in the python file generated by pyuic4
, there is already a function defined called retranslateUi
, and that already contains calls for the QApplication.translate
function for each of the texts in the gui. So this sounds promising for refreshing the gui after a change of language.
For our (very) simple example, we'll make a little gui and make a couple of menu entries to change the language at runtime. Obviously it would be nice to automatically detect the system locale at load time and choose an appropriate language to start with, but changing language at runtime is pretty cool. So it goes like this:
removeTranslator
and installTranslator
on the QApplication object. Additionally call retranslateUi
to update all the gui widgets with the new translationsThis approach can be seen in the following tar file, which includes the example .ui file, the generated .py file, the .ts files and .qm files, and the main program .py file. It also includes a small readme.txt file to explain what each of the files represents.
Update: The standard Qt tool for extracting the texts for translation is called lupdate
, however this only understands .ui files and ignores Python .py files. So to make sure that all the texts are extracted, both static texts from the gui definition and dynamic texts set by Python code, use the python tool pylupdate
(instead of lupdate
) and give it all your .ui and .py files. Note that if you only give it your python files, it will mark the ones from the .ui files as obsolete and won't include them when it releases the texts as a .qm file.
It took a long time to figure this one out because the documentation is effectively non-existent. But, new in version 2.6 of python is finally the ability to zip up a python application (including image files, language resources and so on) so that it can be run as it is without unpacking. It's like python's equivalent of java's jar
format, making a simple and cross-platform way to distribute applications. Why it took them until version 2.6 to see the wisdom of this is unknown, but certainly with version 2.5 it doesn't work. Strangely, even the what's new with python 2.6 page doesn't mention this useful ability at all.
Anyway, the way it should work is simple. Zip up your program, including all .py
files and all the other resources it needs into a single zip file. Make the launch point of your program a separate python file called __main__.py
and then when you run it with the command python zipfile.zip
(using whatever filename you've used for the zip) then the python runtime should find the __main__.py
program and run it. Naturally it should also find all the other python classes in the zip file too and it should all work.
The problem comes with the referenced resources, like images. Let's say you're using PyQt and you've got a QIcon
object made from a QPixmap
. The image file is specified (by QtDesigner) using the relative filename to the image:
icon.addPixmap(QtGui.QPixmap("images/logo.png"),QtGui.QIcon.Normal,QtGui.QIcon.Off)
This works fine when the files are separate, but no longer works when your python files are inside the zip file. For some reason the runtime isn't clever enough to use that relative path inside the archive from which the python class was loaded. So you just get blanks instead of images, and the language resources can't be found either, for the same reason.
The solution, which is far from perfect, seems to be to use the Qt resources function. Sadly the pyqt page on Qt resources is broken with a 404, but the basic ideas are laid out in the example projects supplied with pyqt. In Debian there's a good example at /usr/share/doc/python-qt4-doc/examples/widgets/tooltips/
which you can use as an example.
The idea is to convert the binary resources (in this case a single png) into a kind of python file and associate the converted data with a name. Then, when you want to make the QPixmap object, you don't give the file path to the png but give the name of the object, prefixed with a colon.
The first step is to specify a .qrc
file which is xml and just lists the resources you want to include. The names are optional, so in our case it can be rather simple:
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>images/logo.png</file> </qresource> </RCC>
Now you can use this .qrc
file as an input to the pyrcc4
tool, to convert all the listed resources and write them into a new python file:
pyrcc4 resources.qrc -o resources.py
Now you have a resources.py
file which contains python versions of all your resources. The final step is to import this resources file and modify the QPixmap constructors specifying to use the resources system instead of a filename, like this: QtGui.QPixmap(":images/logo.png"), ...
There's another problem with this workaround though - the QPixmap constructors are defined in the python files generated by pyuic4
so if you edit your ui with QtDesigner and then regenerate your python file, your colons will be lost. So you need to tell QtDesigner to use the resources file, which you can do by loading your .qrc
file, but then pyuic4 adds an import statement with an extra _rc
on the end of the resource name. So to make everything match, you need to make sure that pyrcc4
generates a python file called name_rc.py
, not name.py
.
Finally, if everything still runs, you can zip up your application as before (for example, using the zip
tool from the command line), but now you only need to include the .py
files, and no longer need to add the individual images or .qm
files. As long as the recipient of your zip file has at least python version 2.6, they shouldn't need to unpack the zip file but should be able to just run it as it is. For those with python 2.5, they'll have to unzip it as before and run python __main__.py
.
Needless to say, even though the image files and other resources don't need to be distributed in the zip file, they remain an important part of your source code so they should always be safeguarded, either in a separate source zip or with regular backups. The same goes naturally for the .ui
files, the .ts
files and the .qrc
.
A common problem with writing GUI applications is that the user clicks on a button, and that triggers some task which takes a while. Maybe it's crunching numbers using lots of CPU, maybe it's reading in a large file, whatever it is, it takes time. If this task is done as a result of a button press, it's usually done in the gui-handling thread which should be reacting to other clicks, repainting gui objects and so on - if this thread is busy doing some time-intensive task then the gui doesn't get taken care of during this time. And that of course means you don't see the progress bar being updated properly, you can't press the cancel button and so on. Yuk.
So what you need is for the gui thread to react to the click, realise that this task is going to take a while, and so launch a new thread to do the work. Then you have two threads in parallel, one looking after the gui, and the other concentrating on the task. There are several ways of doing this, and with pyqt you could choose either the python way (using python threads) or the Qt way (using QThreads).
The problems start getting trickier when you try to manage the communication between the two threads. The "worker" thread needs to report back to the gui thread occasionally to tell it how things are going ("I've done 10% of them now"), and the gui thread probably needs to communicate to the worker thread ("Hey, stop now, the user pressed 'cancel'"). And all this needs to be done asynchronously of course, and in a way which keeps both python's and Qt's thread-handling happy. For example, the two threads shouldn't be competing with each other to update the same gui object. And they shouldn't lock each other to prevent each other from running.
There are obviously a few different ways to solve this, but the following solution is reasonably straightforward, relying on Qt's signals to perform the asynchronous inter-thread communication. However (as with many of these pyqt issues) it's not particularly well documented. So here's a small example solution, including the main program .py file and an explanatory readme.txt file.
This example has a very simple gui, a QProgressDialog to show the progress, and a QThread class to do the time-consuming bit (nothing exciting here, just looping). It reports back 10 times, via a signal, and the main class then updates the QProgressDialog accordingly each time. If the cancel button is pressed, or if the progress dialog is closed, this is sent via signal to the thread object and it's told to stop at its convenience. Finally, the thread reports back with yet another signal to say that it has completed.