GUI Programming With Python QT EDITION
GUI Programming With Python QT EDITION
QT Edition
Boudewijn Rempt
GUI Programming with Python: QT Edition
by Boudewijn Rempt
Copyright (c) 2001 by Command Prompt, Inc. This material may be distributed only subject to the terms and
conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the
copyright holder. to the license reference or copy.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior
permission is obtained from the copyright holder. to the license reference or copy.
Although every reasonable effort has been made to incorporate accurate and useful information into this book, the
copyright holders make no representation about the suitability of this book or the information therein for any purpose.
It is provided as is without expressed or implied warranty.
Dedication
This book is dedicated to Irina.
6
Table of Contents
Preface.....................................................................................................................23
1. Who is using PyQt .......................................................................................24
2. For whom is this book intended...................................................................24
3. How to read this book ..................................................................................25
4. Conventions .................................................................................................26
5. Acknowledgments........................................................................................27
1. Introduction ........................................................................................................29
1.1. Python .......................................................................................................30
1.2. GUI programming with Python ................................................................33
1.3. About the BlackAdder IDE.......................................................................35
I. Introduction to the BlackAdder IDE................................................................37
2. Installation....................................................................................................39
2.1. Installing BlackAdder .......................................................................39
2.1.1. Windows ................................................................................39
2.1.2. Linux ......................................................................................40
2.2. Installing sip and PyQt without BlackAdder ....................................41
2.2.1. Building from source on Linux..............................................42
2.2.1.1. Problems with compilation .........................................43
2.2.2. Windows ................................................................................44
3. Interface .......................................................................................................47
3.1. Menubar ............................................................................................48
3.2. Toolbars.............................................................................................48
3.2.1. File toolbar .............................................................................49
3.2.2. Edit toolbar.............................................................................49
3.2.3. Execution toolbar ...................................................................49
3.2.4. Layout manager toolbar .........................................................50
3.2.5. Widgets ..................................................................................50
3.2.6. Help........................................................................................50
3.2.7. Pointer toolbar........................................................................50
3.2.8. More widget toolbars .............................................................51
3.3. Project management..........................................................................51
3.4. BlackAdder Configuration ................................................................53
3.5. Editing...............................................................................................55
3.6. Python shell.......................................................................................55
7
3.7. Conclusion ........................................................................................56
4. Introduction to Python .................................................................................57
4.1. Programming fundamentals ..............................................................57
4.2. The Rules ..........................................................................................61
4.2.1. Objects and references ...........................................................61
4.2.2. Formatting..............................................................................62
4.2.3. Keywords ...............................................................................63
4.2.4. Literals ...................................................................................64
4.2.5. Methods and functions...........................................................64
4.2.6. High level datatypes...............................................................65
4.3. Constructions ....................................................................................66
4.3.1. Looping ..................................................................................66
4.3.2. Branching...............................................................................69
4.3.3. Exceptions..............................................................................70
4.3.4. Classes....................................................................................71
4.4. Conclusion ........................................................................................72
5. Debugging....................................................................................................73
5.1. Running scripts .................................................................................76
5.2. Setting breakpoints............................................................................76
5.3. Stepping along ..................................................................................78
5.4. Debugging Techniques......................................................................81
5.4.1. Avoid changing your code .....................................................81
5.4.2. Gather data .............................................................................81
5.4.3. Minimal examples..................................................................82
5.5. If all else fails....................................................................................82
II. PyQt fundamentals ...........................................................................................85
6. Qt Concepts..................................................................................................87
6.1. Python, Qt and PyQt .........................................................................87
6.2. As simple as they come.....................................................................88
6.3. A better Hello World.........................................................................91
6.4. Designing forms................................................................................96
6.5. Conclusion ......................................................................................101
7. Signals and Slots in Depth .........................................................................103
7.1. The concept of signals and slots .....................................................103
7.1.1. Callbacks..............................................................................104
7.1.2. Action registry .....................................................................106
7.1.3. Signals and slots...................................................................108
8
7.2. Connecting with signals and slots...................................................110
7.3. Disconnecting .................................................................................120
7.4. A parser-formatter using signals and slots......................................127
7.5. Conclusion ......................................................................................137
8. String Objects in Python and Qt ................................................................139
8.1. Introduction.....................................................................................139
8.2. String conversions...........................................................................140
8.3. QCString simple strings in PyQt ...............................................142
8.4. Unicode strings ...............................................................................146
8.4.1. Introduction to Unicode .......................................................147
8.4.2. Python and Unicode.............................................................148
8.4.2.1. String literals.............................................................149
8.4.2.2. Reading from files.....................................................151
8.4.2.3. Other ways of getting Unicode characters into Python
string objects ..................................................................153
8.4.3. Qt and Unicode ....................................................................156
9. Python Objects and Qt Objects ..................................................................159
9.1. Pointers and references ...................................................................159
9.2. Circular references ..........................................................................160
9.3. Qt objects, Python objects and shadow objects ..............................161
9.4. References and ownership ..............................................................163
9.5. Other C++ objects...........................................................................173
9.6. Connecting signals and slots...........................................................173
9.7. Object and class introspection ........................................................175
10. Qt Class Hierarchy...................................................................................177
10.1. Hierarchy.......................................................................................177
10.2. Base classes...................................................................................179
10.3. Application classes........................................................................182
10.3.1. Multiple document windows with QWorkspace................185
10.4. Widget foundations: QWidget ......................................................187
10.4.1. QColor................................................................................189
10.4.2. QPixmap, QBitmap and QImage .......................................190
10.4.3. QPainter .............................................................................191
10.4.4. QFont .................................................................................193
10.5. Basic widgets ................................................................................195
10.5.1. QFrame ..............................................................................197
10.5.2. QPushButton ......................................................................197
9
10.5.3. QLabel................................................................................199
10.5.4. QRadioButton ....................................................................202
10.5.5. QCheckBox........................................................................203
10.5.6. QListBox............................................................................204
10.5.7. QComboBox ......................................................................206
10.5.8. QLineEdit...........................................................................207
10.5.9. QMultiLineEdit..................................................................207
10.5.10. QPopupMenu ...................................................................207
10.5.11. QProgressBar ...................................................................207
10.5.12. QSlider and other small fry..............................................208
10.6. Advanced widgets .........................................................................209
10.6.1. QSimpleRichText, QTextView and QTextBrowser ...........209
10.6.2. QTextEdit ...........................................................................209
10.6.3. QListView and QListViewItem..........................................210
10.6.4. QIconView and QIconViewItem........................................211
10.6.5. QSplitter .............................................................................212
10.6.6. QCanvas, QCanvasView and QCanvasItems .....................212
10.6.7. QTable, QTableItem and QTableView (or QGridView).....213
10.7. Layout managers...........................................................................213
10.7.1. Widget sizing: QSizePolicy ...............................................215
10.7.2. Groups and frames .............................................................216
10.7.2.1. QHBox ....................................................................216
10.7.2.2. QVBox ....................................................................216
10.7.2.3. QGrid ......................................................................216
10.7.2.4. QGroupBox.............................................................216
10.7.3. QLayout .............................................................................217
10.7.4. QBoxLayout and children..................................................217
10.7.5. QGridLayout ......................................................................217
10.7.6. setGeometry .......................................................................220
10.8. Dialogs and Standard Dialogs.......................................................221
10.8.1. QDialog..............................................................................221
10.8.2. QMessageBox....................................................................221
10.8.3. QTabDialog........................................................................227
10.8.4. QWizard .............................................................................227
10.8.5. QFileDialog........................................................................227
10.8.6. QFontDialog ......................................................................228
10.8.7. QColorDialog.....................................................................229
10
10.8.8. QInputDialog .....................................................................230
10.8.9. QProgressDialog ................................................................230
10.9. Qt Utility classes and their Python equivalents ............................230
10.9.1. High level data structures...................................................235
10.9.2. Files and other IO...............................................................238
10.9.3. Date and time .....................................................................239
10.9.4. Mime ..................................................................................240
10.9.5. Text handling......................................................................241
10.9.6. Threads...............................................................................242
10.9.7. URLs .................................................................................244
10.9.8. Qt modules that overlap with Python modules ..................245
11. Qt Designer, BlackAdder and uic ............................................................249
11.1. Introduction...................................................................................249
11.1.1. Starting out with the designer module ...............................249
11.1.2. Creating a design................................................................253
11.1.2.1. Grouping widgets....................................................253
11.1.2.2. Layout management................................................254
11.1.2.3. Tab order and accelerators ......................................254
11.2. Advanced Designer topics ............................................................256
11.2.1. Defining signals and slots in Designer...............................256
11.2.2. Adding your own widgets ..................................................259
11.2.3. Layout management...........................................................262
11.2.3.1. The Horizontal Layout Manager.............................264
11.2.3.2. The Vertical Layout Manager .................................264
11.2.3.3. The Grid Layout Manager ......................................264
11.2.3.4. The Spacer object....................................................265
11.2.3.5. What widgets can do to get the space they want.....265
11.2.3.6. Creating a complex form ........................................266
11.2.4. Generating and using Python code with pyuic ..................268
11.2.5. Generating C++ code with uic ...........................................270
III. Creating real applications with PyQt ..........................................................273
12. Application Frameworks..........................................................................275
12.1. Architecture: models, documents and views.................................275
12.1.1. A document-view framework ............................................277
12.2. Macro languages ...........................................................................284
12.3. Project layout ................................................................................284
13. Actions: menus, toolbars and accelerators...............................................287
11
13.1. Actions ..........................................................................................287
13.2. Menus............................................................................................291
13.3. Toolbars.........................................................................................292
13.4. Keyboard accelerators...................................................................294
13.5. Setting an application icon............................................................295
14. Automatic testing with PyUnit ................................................................297
14.1. About unittests ..............................................................................297
14.2. Starting out....................................................................................299
14.3. A first testcase ...............................................................................300
14.4. Collecting tests in a test suite........................................................302
14.5. A more complicated test ...............................................................303
14.6. Large projects................................................................................306
14.7. Testing signals and slots................................................................309
14.8. Conclusion ....................................................................................312
15. A More Complex Framework: Multiple Documents, Multiple Views ....315
15.1. Introduction...................................................................................315
15.2. Document/View Manager .............................................................319
15.3. The Document Manager ...............................................................325
15.4. Document......................................................................................332
15.5. View ..............................................................................................334
15.6. The actual application ...................................................................335
15.7. Conclusion ....................................................................................348
16. User Interface Paradigms .........................................................................349
16.1. Tabbed documents ........................................................................349
16.2. Back to the MDI windows ............................................................353
16.3. A row of split windows.................................................................354
16.4. A stack of documents....................................................................355
16.5. A more complex view management solution................................357
16.6. Conclusion ....................................................................................360
17. Creating Application Functionality..........................................................363
17.1. Introduction...................................................................................363
17.1.1. Giving the project a name ..................................................363
17.2. The view........................................................................................363
17.3. The document................................................................................368
17.4. Saving and loading documents .....................................................370
17.4.1. Loading ..............................................................................370
17.4.2. Saving ................................................................................371
12
17.5. Undo, redo and other editing functions.........................................372
17.6. Conclusion ....................................................................................378
18. Application Configuration .......................................................................379
18.1. Platform differences......................................................................379
18.2. The Python way of handling configuration settings .....................380
18.3. Implementing configurations settings for Kalam..........................381
18.3.1. Handling configuration data in your application ...............381
18.3.2. Saving and loading the configuration data.........................384
18.3.3. Using configuration data from the application...................386
18.3.3.1. Font settings ............................................................387
18.3.3.2. Window geometry ...................................................387
18.3.3.3. Determining the widget style ..................................389
18.3.3.4. Setting the viewmanager.........................................391
18.3.4. Catching the changes when the application closes ............393
18.4. Settings in Qt 3.0 ..........................................................................394
18.5. Conclusion ....................................................................................397
19. Using Dialog Windows ............................................................................399
19.1. Modal: a preferences dialog..........................................................399
19.1.1. Designing the dialog ..........................................................399
19.1.2. Creating the settings dialog window..................................401
19.1.3. Calling the settings dialog window....................................412
19.2. Non-modal: Search and replace ....................................................418
19.2.1. Design ................................................................................418
19.2.2. Integration in the application .............................................419
19.2.3. Implementation of the functionality...................................422
19.3. Conclusion ....................................................................................435
20. A Macro Language for Kalam .................................................................437
20.1. Executing Python code from Python ............................................437
20.1.1. Playing with eval() .........................................................439
20.1.2. Playing with exec..............................................................440
20.1.3. Playing with execfile() .................................................442
20.2. Integrating macros with a GUI .....................................................443
20.2.1. Executing the contents of a document ...............................443
20.2.2. startup macros ....................................................................452
20.3. Creating a macro API from an application ...................................453
20.3.1. Accessing the application itself .........................................454
20.3.2. Accessing application data.................................................456
13
20.3.3. Accessing and extending the GUI......................................456
20.3.4. Kalam rivals Emacs: an Eliza macro .................................457
20.4. Conclusion ....................................................................................460
21. Drawing on Painters and Canvases ..........................................................461
21.1. Working with painters and paint devices ......................................461
21.1.1. A painting example ............................................................462
21.2. QCanvas ........................................................................................471
21.2.1. A simple Unicode character picker....................................473
21.2.1.1. The canvas...............................................................476
21.2.1.2. The view on the canvas ...........................................478
21.2.1.3. Tying the canvas and view together ........................480
21.3. Conclusion ....................................................................................484
22. Gui Design in the Baroque Age ...............................................................485
22.1. Types of gui customization ...........................................................485
22.2. Faking it with bitmaps ..................................................................486
22.3. Creating themes with QStyle ........................................................491
22.3.1. Designing the style.............................................................491
22.3.2. Setting up ...........................................................................492
22.3.3. A Qt 2 custom style............................................................493
22.3.4. Using styles from PyQt......................................................505
23. Drag and drop ..........................................................................................521
23.1. Handling drops..............................................................................521
23.2. Initiating drags ..............................................................................523
23.3. Conclusion ....................................................................................525
24. Printing.....................................................................................................527
24.1. The QPrinter class .....................................................................527
24.2. Adding printing to Kalam .............................................................528
24.3. Putting ink to paper.......................................................................530
24.4. Conclusion ....................................................................................531
25. Internationalizing an Application ............................................................533
25.1. Translating screen texts.................................................................533
26. Delivering your Application ....................................................................541
26.1. Introduction...................................................................................541
26.2. Packaging source ..........................................................................542
26.3. Starting with distutils. ...................................................................544
26.3.1. setup.py ..............................................................................544
26.3.2. MANIFEST.in....................................................................546
14
26.3.3. setup.cfg .............................................................................547
26.3.4. Creating the source distribution .........................................547
26.3.5. Installing a source archive..................................................550
26.4. Creating Unix RPM packages.......................................................550
26.5. Windows installers ........................................................................551
26.6. Desktop integration .......................................................................552
27. Envoi ........................................................................................................553
IV. Appendices .....................................................................................................555
A. Reading the Qt Documentation.................................................................557
B. PyQwt: Python Bindings for Qwt .............................................................563
B.1. NumPy............................................................................................563
B.2. PyQwt.............................................................................................568
C. First Steps with Sip ...................................................................................573
C.1. Introduction ....................................................................................573
C.2. How sip works................................................................................574
C.3. Creating .sip files............................................................................574
C.4. Things sip cant do automatically ..................................................577
C.4.1. Handwritten code ................................................................577
C.4.2. Other limitations..................................................................580
C.5. Where to look to start writing your own wrappers/bindings..........580
C.6. Sip usage and syntax ......................................................................581
C.6.1. Usage...................................................................................581
C.6.1.1. Invocation, Command Line ......................................581
C.6.1.2. Limitations ...............................................................582
C.6.1.3. Files ..........................................................................582
C.6.1.3.1. Source Files ...................................................582
C.6.1.3.2. Files containing the wrapping .......................582
C.6.1.3.3. Intermediate Files..........................................583
C.6.1.3.4. Auxilliary Files..............................................584
C.6.1.4. .sip File Syntax.........................................................585
C.6.1.4.1. General rules .................................................585
C.6.1.4.2. Macros...........................................................585
C.7. Directives........................................................................................586
C.7.1. Documentation ....................................................................586
%Copying ..............................................................................586
%Doc......................................................................................587
%ExportedDoc.......................................................................587
15
C.7.2. Modules...............................................................................588
%Module................................................................................588
%Include ................................................................................589
%Import .................................................................................590
C.7.3. Conditional Elements ..........................................................590
%If..........................................................................................591
%End......................................................................................591
Version().................................................................................592
%Version ................................................................................593
%PrimaryVersions..................................................................594
%VersionCode........................................................................594
C.7.4. C++ and Header Code Sections ..........................................595
%HeaderCode ........................................................................595
%ExportedHeaderCode..........................................................596
%ExposeFunction ..................................................................596
%C++Code.............................................................................597
%MemberCode ......................................................................597
%VirtualCode.........................................................................598
%VariableCode ......................................................................598
C.7.5. Python Code Sections .........................................................599
%PythonCode ........................................................................599
%PrePythonCode ...................................................................599
C.7.6. Mapped Classes...................................................................600
%ConvertFromClassCode......................................................600
%ConvertToClassCode ..........................................................601
%CanConvertToClassCode....................................................601
%ConvertToSubClassCode ....................................................602
C.7.7. Special Python methods ......................................................602
PyMethods .............................................................................603
PyNumberMethods ................................................................604
PySequenceMethods ..............................................................604
PyMappingMethods...............................................................605
C.7.8. Other....................................................................................606
%Makefile ..............................................................................606
C.8. Accepted C++ / Qt constructs ........................................................606
C.9. SIPLIB Functions...........................................................................609
C.9.1. Public Support Functions ....................................................609
16
C.9.2. Information functions..........................................................609
sipGetCppPtr..........................................................................610
sipGetComplexCppPtr ...........................................................610
sipGetThisWrapper ................................................................611
sipIsSubClassInstance............................................................612
C.9.3. Conversions and argument parsing .....................................613
sipParseArgs...........................................................................614
sipConvertToCpp ...................................................................617
sipMapCppToSelf ..................................................................618
sipConvertToVoidPtr ..............................................................619
sipConvertFromVoidPtr .........................................................620
sipConvertFromBool..............................................................621
sipCheckNone ........................................................................622
sipBadVirtualResultType .......................................................623
sipBadSetType .......................................................................624
C.9.4. Ressource handling .............................................................625
sipReleaseLock ......................................................................625
sipAcquireLock......................................................................625
sipCondReleaseLock..............................................................626
sipCondAcquireLock .............................................................627
sipMalloc................................................................................628
sipFree....................................................................................629
C.9.5. Calling Python.....................................................................629
sipEvalMethod .......................................................................630
sipCallHook ...........................................................................630
C.9.6. Functions specifically for signals/slots................................631
sipEmitSignal.........................................................................631
sipConvertRx .........................................................................632
sipConnectRx.........................................................................634
sipGetRx ................................................................................635
sipDisconnectRx ....................................................................636
C.9.7. Private Functions .................................................................638
Bibliography .........................................................................................................639
17
18
List of Tables
1-1. GUI Toolkits for Python ...................................................................................33
7-1. Matrix of QObject.connect() combinations..............................................119
10-1. Qt and Python high-level datastructures.......................................................235
10-2. Qt and Python network classes.....................................................................245
C-1. C++ access specifiers and sip.........................................................................576
List of Figures
10-1. Qt Inheritance Hierarchy (only the most important classes) ........................177
10-2. Object Ownership Hierarchy ........................................................................179
20-1. Playing with eval() ....................................................................................439
20-2. Playing with exec ........................................................................................441
20-3. Playing with execfile() ...........................................................................442
List of Examples
1-1. Bootstrapping a Python application..................................................................31
6-1. hello1.py hello world ...................................................................................89
6-2. hello2.py a better hello world ......................................................................91
6-3. fragment from hello3.py ...................................................................................94
6-4. Fragment from hello5.py ..................................................................................94
6-5. Fragment from hello4.py ..................................................................................95
6-6. frmconnect.py ...................................................................................................97
6-7. dlgconnect.py the subclass of the generated form .....................................100
7-1. A stupid button which is not reusable ............................................................103
7-2. A simple callback system ...............................................................................104
7-3. A central registry of connected widgets .........................................................106
7-4. Connecting a signal to a slot...........................................................................111
7-5. Connection a dial to a label with signals and slots .........................................113
7-6. Python signals and slots..................................................................................116
7-7. Python signals and slots with arguments ........................................................117
7-8. datasource.py connecting and disconnecting signals and slots .................122
19
7-9. An XML parser with signals and slots ...........................................................128
8-1. qstring1.py conversion from QString to a Python string.........................140
8-2. qstring2.py - second try of saving a QString to a file...................................141
8-3. empty.py - feeding zero bytes to a QCString..................................................143
8-4. null.py - empty and null QCStrings and Python strings .................................144
8-5. emptyqstring.py - feeding zero bytes to a QString .........................................146
8-6. Loading an utf-8 encoded text ........................................................................151
8-7. Building a string from single Unicode characters ..........................................153
8-10. uniqstring1.py - coercing Python strings into and from QStrings ................156
8-11. uniqstring2.py - coercing Python strings into and from QStrings ................157
9-1. refs.py - showing object references ................................................................160
9-2. circular.py - circululululular references..........................................................161
9-3. qtrefs1.py about Qt reference counting .....................................................163
9-4. qtrefs2.py - keeping a Qt widget alive............................................................164
9-5. qtrefs3.py - Qt parents and children ...............................................................165
9-6. Eradicating a widget .......................................................................................166
9-7. children.py - getting the children from a single parent...................................167
9-8. Iterating over children.....................................................................................169
9-9. sigslot.py - a simple signals/slots implementation in Python, following the
Observer pattern.............................................................................................173
9-10. Object introspection using Qt .......................................................................175
9-11. Object introspection using Python................................................................176
10-1. event1.py - handling mouse events in PyQt..................................................180
10-2. action.py - Using a QAction to group data associated with user commands183
10-3. fragment from mdi.py - ten little scribbling windows..................................186
10-4. event2.py - using QWidget to create a custom, double-buffered drawing
widget.............................................................................................................187
10-5. snippet from event3.py - a peach puff drawing board ..................................190
10-6. fragment from action2.py - You cannot create a QPixmap before a
QApplication..................................................................................................192
10-7. buttons.py - Four pushbuttons saying hello. ..............................................198
10-8. label.py - a label associated with an edit control ..........................................199
10-9. radio.py - a group of mutually exclusive options .........................................202
10-10. listbox.py - A listbox where data can be associated with an entry .............204
10-11. tree.py - building a tree...............................................................................210
10-12. layout.py - two box layouts and adding and removing buttons dynamically to
a layout...........................................................................................................218
20
10-13. geometry.py - setting the initial size of an application ...............................220
10-14. dialogs.py - opening message and default dialogs boxes ...........................222
10-15. fragment from dialogs.py - opening a file dialog .......................................228
10-16. fragment from dialogs.py - opening a font dialog ......................................229
10-17. fragment from dialogs.py - opening a color dialog ....................................229
10-18. from dv_qt.py - using Qt utility classes......................................................231
10-19. fragment from db_python.py - using Python utility classes.......................233
10-20. Using QMimeSourceFactory (application.py)............................................241
10-21. thread1.py Python threads without gui ..................................................242
10-22. Python threads and a PyQt gui window......................................................243
11-1. dlgcomplex.py a subclass of frmcomplex.py ..........................................268
11-2. Setting default values....................................................................................270
12-1. A simple document-view framework ...........................................................277
12-2. Scripting an application is easy ....................................................................284
13-1. Defining a complex toggle action .................................................................288
15-1. A testcase for a document manager..............................................................319
15-2. The document manager class........................................................................325
15-3. The document class ......................................................................................332
15-4. The view class ..............................................................................................334
15-5. The application class ....................................................................................336
21-1. typometer.py - A silly type-o-meter that keeps a running count of how many
characters are added to a certain document and shows a chart of the typerate...
462
21-2. charmap.py - a Unicode character selection widget .....................................475
22-1. remote.py - remote control application.........................................................488
22-2. view.py - the main view of the remote control application ..........................489
22-3. button.py - the class that implements the pixmapped buttons ......................490
22-4. A Qt 2 custom style - a minimalist implementation of the classic Mac style in
PyQt. ..............................................................................................................493
22-5. Testing styles ................................................................................................506
23-1. Handling drop events....................................................................................521
23-2. Drag and drop ...............................................................................................524
25-1. Installing the translator .................................................................................538
26-1. README .....................................................................................................543
26-2. setup.py - a sample setup script ....................................................................544
26-3. MANIFEST.in ..............................................................................................546
C-1. Interface for QRegExp::match .......................................................................616
21
22
Preface
The main topic of this book is application development using PyQt, a library
extension to the Python programming language a library that is meant to form
the basis for GUI programming. PyQt is free software, but there is also a
commercial IDE available, BlackAdder, that is specially written to assist working
with PyQt. I will show you the ins and outs of PyQt by developing a complete and
complex application.
Like most thirtysomethings who started programming in their teens, Ive worked
with a lot of different technologies. I started with Sinclair Basic, going on to Turbo
Pascal and SNOBOL I have developed for Windows in pure C, with Borland
Pascal and with Visual Basic. Ive done my stretch with Oracle Forms, and served
as a Java developer. On Linux, Ive wet my feet with Free Pascal, with C++, using
XForms and Qt. And just when I was getting fond of Qt and C++, I found out about
Python a few years ago now. I found programming with PyQt to be a lot more
fun than anything else, and productive fun, too.
For sheer productivity, nothing beats Python and PyQt. And while theres always
something new to learn or explore in Python, if youre in the mood, its easy and
pleasant to write useful applications from the first day. No other programming
language or library has ever given me that.
So, when Cameron Laird, during a discussion on the comp.lang.python newsgroup
suggested that Id write a book on this particular way of developing GUI
applications with Python, I started to think and more than think. I started to
contact publishers, until one day Shawn Gordon of TheKompany brought me into
contact with Joshua Drake of Opendocs. I started writing text and code almost
immediately.
Joshuas patience has been monumental I should have written this book between
February and May, but it took me until November. All I can say for myself is that a
lot of effort has gone into the book. I discuss most of the concepts and classes of the
Qt library, which might be useful not only to Python developers, but also to C++
developers, and I have written a lot of example scripts.
Where Bruce Eckel (of Thinking in Java fame) favors small example programs
because they clearly illustrate the matter in hand, John Grayson in Python and
Tkinter argues that larger real-life applications are more useful because they dont
23
Preface
24
Preface
your chosen language is Python. The same holds for the extensive html
documentation that comes with the C++ Qt library.
With the growing popularity of Python, PyQt and BlackAdder, people will start
using these tools who dont want to translate C++ to Python to figure out what they
are supposed to do.
This is the first group of people for whom Ive written this book: beginning software
developers who have chosen Python because it allows them to become productive
quickly with a language and an environment that have been designed to
accommodate subject specialists. That is, people who need to get an application
done to help them with their work, but who are not developers by profession.
Then there are the experienced developers, people who have been coding in Visual
Basic, Delphi or Java, and who, like the first group, now need something a lot more
productive and portable. They will be able to grasp the concepts quickly, but may
find a lot of use in the advanced code examples and the in-depth discussion of issues
particular to PyQt.
Another group of possible readers consists of C++ developers who have turned to
Python as a rapid prototyping language. Prototyping with the same GUI library they
will use for their final C++ application will give them a definite advantage, as most
of the prototype code will remain useful.
Finally there are people who are more experienced in Python than I am, but who
want to get acquainted with one of the best-designed GUI toolkits available for the
languagethere is a lot of interesting content to be found in this book for them, too.
My aim in writing this book was to create a genuine vademecum for Python, PyQt
and GUI programming. If you keep referring to this book a year after youve
acquired it, if you can find the answer to most of your daily and quite a few of your
exceptional problems, and if you tend to keep this book in the vicinity of your desk,
then I will have succeeded.
25
Preface
a small chapter that introduces programming with Python, in case you are not
already familiar with the language.
The second part deals with the concepts behind Python and PyQt. You dont need to
read this part in order, but the chapters will give you a solid feel for the lay of the
land, and will enable you to find your way in the PyQt or Qt class documentation
(which is copious and excellent). Also, if you run into inexplicable behavior, you
might want to consult, for instance, the chapter on objects and references. The order
of the chapters doesnt matter a whole lot.
Part three is where the real fun starts. From humble, but solid, beginnings, we will
build, chapter by chapter, a very real application. This part is probably best read in
order, but there are occasional excursional chapters that you might want to read
before anything else, such as the chapter on unit testing.
Finally, there are the appendices. Appendix A is useful if you dont know anything
about C++, but still want to read the C++-based Qt documentation. The second
appendix, Appendix C, tells you how to wrap your own C++ extension libraries
possibly based on Qt using sip, the same tool that is used to create PyQt.
Appendix B deals with PyQwt and NumPy, an extension library for plotting and
graphic.
4. Conventions
Code is always printed in a monospaced font - like this:
class Test:
def printTest(self):
print self
This also holds for references to bits of code in the running text. If I cite a function
in the text, it is done like this: printTest() i.e., I generally dont quote the
parameter list. This makes it easier to follow the run of the text
Even though PyQt is a cross-platform toolkit (and Ive tested most of the examples
on Windows, too), all development has been done on two Linux computers: my
laptop maldar, and my main system, calcifer, named after one of the main
characters in Diana Wynne Jones Howls Moving Castle. Because BlackAdder
26
Preface
wasnt ready when I wrote this book, I used XEmacs and Bash (the command line
shell) to create and test all examples. Thats why you will often see prompts in
illustrations, and not so often screenshots of BlackAdder:
If you are using Windows, you can use a DOS box to emulate the Bash shell, but it
wont be quite as convenient.
Finally, it is a widely-honored convention in programming literature, and especially
in Python books, to make allusions and puns that are related to the punny names of
the product. By rights I should have filled my code with witty allusions to the British
comedy series Monty Python and BlackAdder. However, excellent and essential as
these are, its been long years since I last watched those on the television, and I
dont feel quite up to it. Ive done my best, but dont expect too much!
A note on versions: when I wrote this book I mostly used the stable 2.x versions of
Qt, but as soon as betas of Qt 3.x became available, I started integrating information
about its improvements in the text. I will note wherever one version is different
from the other. On the Opendocs webforum for this book youll find versions of the
examples both for Qt 2.x and Qt 3.x.
5. Acknowledgments
Writing a book started out fun, but it soon became far more work than I imagined.
My wife, Irina, and my children, Naomi, Rebecca and Menna were very patient
with me when I locked myself in the study day after day, night after night. But my
children have asked me never to write a book again. For now Im inclined to agree,
but well see.
Phil Thompson wrote the software that this book is all about and has fixed bugs
faster than I could write chapters. Cameron Laird is responsible for egging me on to
start writing, and Shawn Gordon for introducing me to Joshua Drake, who dared to
27
Preface
take the book on. Michael Holloway, the editor, has fixed lots of bad English and
made this book a better book.
Neelakantan Krishmaswami is ultimately responsible for getting me to look at
Python at all everyone needs someone else to help him over the
indentation-is-block-marking hurdle, and Neel helped me.
Jim Bublitz and Wilken Boie have contributed largely to Appendix C Jim by
writing the introduction, and Wilken by writing the overview of directives. Gerard
Vermeulen wrote Appendix B. Ive been editing their texts, though, so any mistakes
are mine. Cameron Laird gave persmission to use the graphics of a remote control
for Chapter 22. Bruce Sass took the time to explain about Debian packaging. Steve
Purcell helped with the chapter on unit-testing.
The following people have helped me learn about Python, Qt and the combination
in the past years, on the PyKDE mailing list (set up by Torsten Horstmann) and the
Python newsgroups: Aditya Bhambri, Albert Wagner, Anshul Shekhon, Arun
Sharma, Corrin Lakeland, David C. Morrill, David Eller, Deirdre Saoirse, Dirk
Reisnauer, Henning Schroeder, Johannes Sixt, Jonathan Perez, Karan Vasudeva,
Maik Roeder, Marco Bubke, Martin P. Holland, Neal Becker, Pete Ware, Peter
Torstensen, Reiner Wichert, Richard Jones, Steve Noble, Toby Sargeant and Gerrit
Sere.
Finally, many people have read the drafts and helped me write a better book by
sending me their comments - sometimes very long and detailed: Andre Gosselin,
Andy Anderson, Brent Burley, Christopher Farly, Damon Lynch, Dave Turner,
Dincer Aydin, Mark Summerfield, Robert Hicks, Sean Ahern and Yigal Duppen.
28
Chapter 1. Introduction
Developing decent software is difficult monstrously difficult, in fact. People are
always looking for miracle cures, silver bullets that will help them creating great
software in no time with no conscious effort. In fact, almost everyone will agree to
the existence of a software crisis. Projects do deliver too little functionality, too
late and often of a too low quality. Frederick Brooks was the first to note this, in his
famous book The Mythical Man-Month. Mores the pity that there arent any
miraculous solutions for the many problems that plague software development.
There is simply no single innovation that will make you ten times more productive,
no single innovation that will ensure that whatever you do, you will produce
bug-free software and no single innovation that will make your applications run will
all the vim and vigor your users desire and deserve.
However, it is quite possible, by simply using the best possible tools and practices,
to be far more productive than would be possible by following the usual practices
and by using inferior tools.
Its amazing how many software development environments have been designed
with something else than developer productivity as the main goal. Theres Visual
Basic, which, while infinitely more productive than previous attempts at creating a
rapid development environment for Windows, still is mainly concerned with
preventing people from creating applications that can compete with Microsofts
own applications. Java, while quite usable, tries far too hard to protect me from
myself and my colleagues like early versions of Pascal. C++ is enormously large
and complicated, because of its compatibility goals with C almost too big to
learn to handle. In contrast, Python was designed to be small, practical and to be as
open as possible to the developer.
In Python, all other considerations, are secondary to considerations of development
speed, code maintainability and code reusability.
Python offers everything you need to put the best practices into practice, like object
oriented design, unit testing and maintaining documentation in the code, but it
doesnt keep you from messing with the more messy parts of the operating system
you can always use an extension module written in C or C++ or with the
internals of Python itself. It is ideal for rapid prototyping, but also for the
development of large applications by large teams of programmers.
29
Chapter 1. Introduction
Python code is meant to be readable. Indenting correctly and neatly is not merely a
good habit: it is essential to delimit blocks of code. Likewise, there is little use for
comic-book swearing characters like !@#$#%$ that other languages use to
indicate the type of a variable, or even for variable declarations and all those other
things that keep you from writing the logic of your application. The most famous
description of Python is that its executable pseudo-code!
However, what Python has been lacking until recently was a good development
environment. Of course, since all Python code is simple text, and since you dont
need pre-processors or compilers, you can get by with nothing more than a text
editor, like XEmacs Nedit, or MultiEdit. Indeed, Ive used Nedit exclusively for
years but some project management facilities, a tighter integration with a GUI
builder and a good debugger can make life infinitely more pleasant, and thus
productive.
BlackAdder is such an environment. Others are Wing IDE, PythonWorks,
PythonWin, Komodo and, perhaps, IDLE. Of these, only BlackAdder runs on both
Windows and Linux, includes a full-featured GUI designer and provides a
dependable debugger. Applications developed with Python and BlackAdder can run
on any Unix platform with X11 and on any 32-bits Windows platform (and in the
near future on Apples OS X, too).
1.1. Python
Python is a modern programming language, with strong object-oriented features, a
small set of basic functions and large set of libraries. The most important features of
Python are:
30
Chapter 1. Introduction
#!/usr/bin/env python
#
# bootstrap.py
#
import sys
from myapp import SomeClass
def main(args):
class=SomeClass(args)
class.exec_loop()
if __name__=="__main__":
main(sys.argv)
31
Chapter 1. Introduction
The so-called hash-bang trick is useful on Unix systems only. If the first line
of any text file starts with #!, then the system will try to execute the application
that follows the #! with the rest of the file as input. In this case, the env utility
starts python, which runs the rest of the script.
The standard Python module sys handles tasks like passing on command-line
arguments and lots of other things. Here we import the module, so we can pass
the command-line arguments to the application.
All application code is in separate modules; the first of these we import here.
This is the definition of the main function. By encapsulating this code in a
function, it wont get run if this file were imported from another file.
In this line, we check if this is a top-level script, instead of a file imported from
another file. This is done by looking at the variable __name__. If this is the
toplevel file, then the main(args) is run.
Python is, like Java, a language that is compiled to bytecode. Python uses a virtual
machine to run the bytecode. This virtual machine is written in C and interprets
each byte-code instruction, translates it to real machine code and then runs it. The
Python virtual machine differs from the Java virtual machine in that the byte-code
instructions are a bit more high-level, and that there are no JIT-compilers that
pre-compile chunks of byte-code to native machine code.
The translation from Python code to byte-code only happens once: Python saves a
compiled version of your code in another file with the extension .pyc, or an
optimized compiled version of your code that removes assert statements and
line-number tracking in a file with the extension .pyo.
However, that is only done with Python files that are imported from other files: the
bootstrap script will be compiled to bytecode every time you run it, but python will
create a myapp.pyc from a file myapp.py (which is not shown here).
Interpreted languages, even byte-code interpreted languages, have a reputation for
sluggishness. On the other hand, modern computers have a well-deserved reputation
32
Chapter 1. Introduction
for excessive processing power. The combination means that an application written
in a interpreted language can be fast enough for almost any needs.
Certainly, anyone who has ever tried to use a full-scale Java GUI application will
know the exact meaning of the expression slow as frozen treacle. There are several
reasons for the abominable slowness of Java applications, the most important of
which is the fact that all Java Swing gui elements are also written in Java. Every
pixel is put on screen by Java. Python, on the other hand, makes clever use of
available GUI libraries that are coded in C or C++ and thus run as native machine
code.
The ease with which Python can make use of native libraries is one of its strong
points. Thanks to this extensibility, you can write the logic of your application in
Python, and later rewrite the bottlenecks in C or C++. But even without writing
extension libraries, I have never encountered any problem with the performance of a
Python application.
33
Chapter 1. Introduction
Tkinter Yes Yes Yes, Tkinter is the most ancient Python GUI
mostly toolkit. It is based on tcl/tk, and has
neither the real platform UI look and feel,
nor a real Python programming style. A
good resource is John Graysons book,
Python and Tkinter programming.
PyQt Yes Yes OS X PyQt is based on Qt, the cross-platform
only GUI toolkit by Troll Tech. Its also, not so
coincidentally, the subject of this book.
wxPython Yes Yes No wxPython is based on the wxWindows
toolkit. wxWindows is a crossplatform
wrapper around a native toolkit of each
platform: the standard Win32 controls on
Windows and GTK on Unix/X11.
FxPy Yes Yes No One of the smaller - in terms of user base
- toolkits, it is based on the FOX toolkit.
FxPys main feature is execution speed.
PyGTK Yes (a bit)Yes (If you PyGTK is based on GTK (formerly
(+PyG- run a known as the Gimp Toolkit). Not really
nome) separate intended for cross-platform work, it has
X Server recently been ported (more or less) to
on OS X) Windows.
Python- Yes No No Pythonwin is the - rather
win underdocumented - binding to Microsofts
MFC library. Its not portable, of course.
There are many others GUI toolkits available, both dead and alive. For a complete
listing, please see Cameron Lairds notes on Python GUIs at:
http://starbase.neosoft.com/~claird/comp.lang.python/python_GUI.html. However,
the really serious options for someone selecting a toolkit are Tkinter, PyQt and
wxPython. I have selected PyQt for my own use, based on criteria of performance,
34
Chapter 1. Introduction
35
Chapter 1. Introduction
There is also a simple but dependable debugger, a Python interpreter window for
when you want to make a quick test, and last, but not least, an excellent gui designer.
Especially the gui designer is worthy of serious attention. It is based on Qt
Designer, which is a standard part of the Qt library. It produces designs that can, at
your choice, be transformed into executable Python code or compilable C++ code.
This means that if you prototype your application in BlackAdder and later, for
whatever reason, decide to move it to C++, you can keep all the interface work
youve done already.
Using Python and PyQt does not force you to use BlackAdder: you can, if you live
in the Unix world, use the free, GPL, version of Qt, which includes the original Qt
Designer, the free version of PyQt and Python, to create the same applications. On
Windows or OS X, you can use the non-commercial version of Qt with the free
PyQt binaries - these cannot be used to develop commercial applications, or in a
in-house commercial setting, but are completely identical to the Unix/X11 GPL
library in all other respects.
The GUI design files Qt Designer produces and those of BlackAdder are completely
compatible. Likewise, using BlackAdder doesnt force you to use PyQt - you can
just as well create a Tkinter application with BlackAdder. You wont find much use
for the Designer module though, since that only knows about the Qt widgets.
All in all, BlackAdder combines all the tools you need to develop good GUI apps in
an extremely convenient package, and the added productivity of this system is well
worth the small expense, especially if you intend to develop commercial
applications on Windows.
36
I. Introduction to the
BlackAdder IDE
Table of Contents
2. Installation ..........................................................................................................39
3. Interface ..............................................................................................................47
4. Introduction to Python ......................................................................................57
5. Debugging ...........................................................................................................73
There are several possibilities for starting out with Python and PyQt. You can buy
BlackAdder, the PyQt IDE, or you can download the freely available components
Python, Qt and PyQt and use your own tools to write your application.
In this part Ill first guide you through the installation of BlackAdder or PyQt. Then
we make a brief tour of the interface of BlackAdder. A very short introduction to
Python and the first concepts of programming follows, and we conclude with a
chapter on using the BlackAdder debugger.
Chapter 2. Installation
In this chapter I briefly describe how to install BlackAdder on Windows and Linux.
After that, compiling PyQt from source is described in a little more detail.
Of course, a book generally has a longer life than a certain version of a software
package, so installation details might have changed between time of writing and
time of buyingso dont forget to read the README files!
2.1.1. Windows
To install BlackAdder on Windows you need the following components:
Python. (Be careful to choose the version of Python that is right for your version
of BlackAdder.)
BlackAdder.
The BlackAdder Qt module.
And eventually, the Egenix MX ODBC module, if you want to do database work.
Now its simply a matter of installing the components, one after another. Every
component is provided in a comfortable Windows installer package.
39
Chapter 2. Installation
Installing BlackAdder
BlackAdder will now be ready to run a friendly icon has appeared on your
desktop, just begging to be clicked.
2.1.2. Linux
There are rpm packages for a lot of distributions: Mandrake, RedHat and SuSE.
Additionally, there is a .tgz package for Slackware.
Installing BlackAdder does not differ from installing any other package for your
favorite distribution you can use a fancy gui like KPackage, or type
on the command line, if youre installing the 3.1 beta for SuSE Linux. The actual
name of the rpm will vary, of course.
40
Chapter 2. Installation
Additionally, you might want to set two environment variables in your .bashrc
file. Installing BlackAdder and Python in the default location isnt necessary, but if
you deviate from the standard BlackAdder installation directory, you need to set the
following variables.
Now, simply typing "ba" on the command line will start BlackAdder.
41
Chapter 2. Installation
BlackAdder
All components that combine to form a PyQt development environment are also
freely available. Python, Qt, Qt Designer, sip, PyQt and editors are all available as
open source. If you use Linuxand particularly if you use a modern and complete
distribution like SuSE or Redhateverything you need is included on your
distribution media, including PyQt. There are also Debian packages of PyQt
available. Installing these ready-made packages is very easy, but they are not always
completely up-to-date. In the next section, I will discuss building PyQt from source,
which is necessary if you want to always run the latest version of PyQt.
Windows users who want to use PyQt without BlackAdder have some downloading
to do, but all components are available as binary packages that come with easy to
use Windows installers.
Installing PyQt from source on Windows falls outside the scope of this book, partly
because it is quite complicated, and partly because I dont have a C++ compiler for
Windows. The Qt library essentially demands Visual C++.
You can also access the PyQt CVS repository (the central place where the most
current code is kept also at http://www.thekompany.com). Compiling PyQt from
CVS source entails creating the C++ bindings code from the sip definition files, and
then carrying on as if you had downloaded the source. Keep in mind that CVS
versions of software are not expected to work!
sip
PyQt
Eventually: PyKDE
Be careful to choose versions of packages that fit each other. You can compile PyQt
with most versions of Qt, but Python 2.1 will give better results than Python 2.0,
and so on.
42
Chapter 2. Installation
You need to compile and install sip before compiling PyQt. After unpacking the sip
tar archive, you will need to give the following commands:
If your Python or Qt installation is in an odd place, then chances are that the
configure script cannot find it. In that case, you should give their locations on the
command line:
This will build the sip library and executable, and install them (most likely in
/usr/local/). With this done, it is time to do the same with PyQt. This time, the
make command will take a long time to run, because PyQt is a very large set of
bindings, and the GNU C++ compiler isnt the fastest around.
The whole process might take a while, but should not pose any problem.
Sometimes, however, things go wrong...
43
Chapter 2. Installation
If you are experiencing problems, you probably have several versions of Python or
Qt on your system, and the compilation configuration process inevitably picks the
wrong one for instance Qt 2.3.1 for sip and then Qt 3.0.0 for PyQt. Murphys law
cannot be avoided! This hurts compilation but is easily avoided by giving the
right versions at the ./configure command line.
If you have determined that this is not the problem, your best bet will be to
subscribe to the PyQt mailinglist: http://mats.gmd.de/mailman/listinfo/pykde,
where most of us have found succor more than once.
2.2.2. Windows
You can develop applications on Windows systems with PyQt using only gratis
software. Life will certainly be more difficult than if you buy BlackAdder, because
you miss the nice integration of editor, debugger and operating system. Another
issue is licensing: if you buy the professional edition of BlackAdder, you can write
commercial software. If you use the non-commercial version of the Qt library and
the separately available packages of sip and PyQt, you are not allowed to sell your
programs: you are not even allowed to use your software yourself in a commercial
setting. However, if you want to develop PyQt on windows without spending any
money, you need the following components:
44
Chapter 2. Installation
SciTE, which uses the same editor component as BlackAdder, is very powerful and
pleasant to use. Get SciTE from http://www.scintilla.org/SciTE.html. (SciTE is also
available for Linux.)
You job is then reduced to manually installing Python, Qt, PyQt, and an editor.
Then you can get started on developing your application.
45
Chapter 2. Installation
46
Chapter 3. Interface
In this chapter we quickly walk through the various components of the BlackAdder
IDE. Once you have BlackAdder installed, you can start it by typing ba at the Unix
shell prompt, or double-clicking the new icon on your desktop. Youll be greeted by
a stylish splash screen, followed by the BlackAdder application window.
Note: Please keep in mind that this chapter was written using the beta version
of BlackAdder. As a result, certain aspects of BlackAdder were not yet in their
final form. For instance, the toolbar icons are expected to change.
If, on the other hand, you are confronted by a window telling you that the Python
interpreter has died, you will probably need to correctly set the path to the Python
47
Chapter 3. Interface
However, if all is well, you can start exploring your new development environment.
3.1. Menubar
The BlackAdder menubar combines functionality for the editing of Python source
code and the creation of GUI forms.
The Tools, Layout and Preview menus contain commands for the creation of
forms, while Run is used to start and debug Python scripts. File and Edit have
commands for both tasks.
You can hide or unhide various important parts of BlackAdder with options from
the Window menu. These are the project Explorer, the Property Editor, the Object
Hierarchy window, the Traceback viewer and the Python interpreter window.
3.2. Toolbars
BlackAdder possesses an imposing array of toolbars. Id certainly advise you to not
emulate this example in your own application!
48
Chapter 3. Interface
Most of the toolbars are used when designing forms, as they represent different
widgets and layout strategies. These widget buttons really belong in a floating tool
palette, but lets gloss over that user interface design nicety.
Going from left to right and top to bottom on the previous image, we encounter the
following toolbars:
49
Chapter 3. Interface
3.2.5. Widgets
Next follows a set of buttons that insert display widgets in your design: a textlabel, a
picture label, and LCD number, a line, a progressbar, and finally, a textview and a
more complex textbrowser (this is a small html browser in itself). These last two
buttons are used to insert rich text widgets.
3.2.6. Help
The lonely button on the last toolbar of the second row gives you help if you first
click on the button, and then on the object you want help with. By clicking it and
then subsequently clicking on all toolbar buttons, youll discover that I havent been
lying to you in this section.
50
Chapter 3. Interface
You can also add your own widgets to the palette. If you do so, you will discover
BlackAdder has given you a new, and last, button bar.
51
Chapter 3. Interface
In this example, we create a project for the editor we develop in Part III of this book.
After you have created the project, you can add items to the project by choosing
File-Add file to project, or by creating new files.
Project files (ending in the .bap extension) are saved by default in the directory you
started BlackAdder from. Using File-Save Project As, you can save your project
file wherever you want.
The project explorer shows all files that are part of your project, and all Python files
that can be found on the default Python path.
52
Chapter 3. Interface
By choosing Window-Close Project you can close your project again. If you close
BlackAdder and restart it, it will automatically open your projects again. Every
project has its own window in BlackAdder. You can have more than one project
open at a time, each in a separate MDI window, and each with its own set of panes.
(Again, this is not a good example to emulate; see Chapter 16 for a discussion of
document/view paradigms).
53
Chapter 3. Interface
As you can see, there are six tab panels: General, Python, Ruby, Editor, Form Editor
and Debugger. In its current incarnation, BlackAdder supports not only Python, but
also another scripting language: Ruby (http://www.ruby-lang.org/en/). Ruby was
created by the Japanese hacker Yukihiro Matsumoto, and is quite akin to Python. In
this book I dont concern myself with the Ruby bindings to Qt.
Most of the options are quite self-explanatory. You can ask BlackAdder to not show
its splash screen, to automatically open the most recently opened project file, to
alter the font used in the editor and interpreter windows, to show a grid in the forms
editor, and whether or not to show certain categories of variables in the debugger.
One thing I advise you to change is the default indentation width in the Editor tab.
Python source code is best appreciated when you write it using an indentation level
of four spaces and no tabs. The auto-indent is also helpful.
54
Chapter 3. Interface
3.5. Editing
The BlackAdder editor is based on Neil Hodgsons Scintilla editor, which is also
available for Linux and Windows as a stand-alone editor, called SciTE
(http://www.scintilla.org/SciTE.html).
This editor has a few very nice features. The syntax highlighting for Python (and
many other languages) is nearly instantaneous. It also offers auto-indent. This
means that you never have to press TAB to indent another level the editor knows
when to indent based on the contents of the previous line.
You can also fold in functions and classes. In the gray vertical bar on the left of the
editor pane, there are small horizontal lines, akin to minus signs. These correspond
to class, def and other block statements. You can click on the minus sign, and
suddenly the whole block disappears under its first line, and a thin line is drawn
over the width of the pane, indicating the fold:
Folding in action.
The Scintilla editor component has other interesting features, such as Python
tooltips, but these werent integrated in the beta version of BlackAdder I used to
write this book.
55
Chapter 3. Interface
BlackAdder doesnt have an output window, but if you run your scripts in the
debugger, all output will appear in the Python shell window.
Even more interestingly, your debugged script runs in the same interpreter as this
window. This means that if you are debugging a script, you can alter the value of
any variable from the shell window, just by assigning it. You can even call class
methods or other functions.
3.7. Conclusion
That concludes the tour of the BlackAdder interface - at least, the interface as it was
during the last Beta. BlackAdder is quite a traditional IDE, and it should take no
effort at all to get comfortable with it.
56
Chapter 4. Introduction to Python
In this chapter I attempt to explain the fundamentals of Python. Here I have the
same difficulty as Bertie Wooster faces when he tries to keep us abreast of the
developments in Much Obliged, Jeeves. If I start too early, and begin at the very
beginning, telling you all about how a computer doesnt understand plain English,
Im likely to irritate the coves who already know all about that, and just want a
quick update on the high-level datastructures of Python and the current state of
iterators and generators. However, that would leave the birds who are just starting
out wondering whether it was such a good idea, after all, to pick up this book, and
start learning how to program.
The fact is, writing an introduction to a complete programming language or the
concept of programming in itself in just one chapter is the deuce of a task. It
cant really be done, Im afraid to say. If you already know a few programming
languages, the on-line Python tutorial that is included with BlackAdder (or with
Python itself) will probably suffice. If you havent programmed all that much
before, I highly advise you to buy Marc Lutz excellent book, Learning Python,
which is more like an introduction to programming, with a focus on Python.
Still with me? Then we had better take a quick tour through Python which is
really one of the easiest programming languages to master. Like ancient Gaul, and
like this book, I have divided this chapter into three sections. The first tries to gently
introduce the concept of programming to people who need to be primed with the
most basic concepts. This is difficult for me to do, because I have been
programming since I was twelve years old, so bear with me. The second is about
Rules. Every programming language needs rules, and these are the rules that you
need to keep in mind while programming Python. The final part gives an overview
of the various constructions that Python contains for your satisfaction and pleasure.
57
Chapter 4. Introduction to Python
always told the machine what to do. Even crashing, down to the ultimate Blue
Screen of Death, is caused by a computer blindly following instructions given by a
human being.
Instructions can take the form of mouseclicks on fancy icons or buttons, or of bits of
text the computer can understand. While there is still no computer that can
understand plain English, there are many sub-dialects of English that a computer
can understand. Python is one of these a mix between pidgin English and
mathematical notation. It is close to both the way computers work, and the way
people think.
Unless you have a speech-to-text interface for your computer, you will have to type
out all the pidgin-English, and then tell the computer to read what youve written,
and do what you told it to. In a sense, you have to write a kind of manual for the
computer to read, on how to perform a certain task.
Lets start with a simple example: fire up BlackAdder, and open the Python
Interpreter window. If you start typing at the >>>, nothing will happen only by
pressing the Enter key will Python realize that it has been spoken to. Go ahead and
type something you cant hurt the computer or your system, except if, by a fluke,
you type import os, followed by Enter and os.system("deltree c:")
which would radically clean out your C drive. So dont do this! On the other
hand, asking Python about the captains age or the contents of a bathtub thats being
filled by two taps is all right.
Chances are very small that you will have hit upon something Python understands
by accident, for you are strictly limited to the few keywords Python actually knows
about. Most of these keywords are concerned with creating blocks of instructions,
called functions. Functions are used to construct more complex systems. Other
keywords are used for creating another kind of block, called classes, which are
combinations of information and instructions.
Lets construct a class that knows the value of something (though not the price), and
has a function that does something to that value. Remember to press enter at the end
of each line, and dont type the three > signs or the three dots Python does this
for you.
58
Chapter 4. Introduction to Python
If you type neatly and without mistakes, the contents of the Python interpreter
window might look like this. Lets look at what happens: we have defined a class
thats a combination of information and complex actions that work on the
contained information. The class has a name: FirstClass. (It is customary to
capitalize the first letter of each word in a classname).
A class in itself is only the template, so to speak, while an object is the
document just as you can make documents out of templates in a
wordprocessor, you can make objects from classes.
Furthermore, the class has two functions defined with the def statement.
The first function, __init__, is called when you want to create an object. The
function has two parameters that is, two names associated with a value (which
we call a variable because the value can change, though the name remains the
same). The first parameter refers to the object its always called self in Python
(though it is called this in Java or C++). The second parameter is the value we
want the object to manage for us.
You can use a dot . to associate variables with each other. The line self.item =
value means that from now on the object we refer to with self (but also, in
another context, with firstObject) knows that the name item is associated with
the value represented by the parameter value.
Cleverly, Python doesnt forget this, so when you create an object with the name
firstObject and the string value (that is to say, some text, as opposed to a
59
Chapter 4. Introduction to Python
number) BlackAdder goes forth, you can later call the printValue() function,
which will be able to do something with that value.
In order to callthat is, ask Python to execute a function, you must add brackets
after the function name; the parameters always go between the brackets. You dont
have to put self between brackets, for Python does this for you. If you dont add
the brackets, you are referring to the function, not asking Python to execute it.
Python then answers you with the revealing sentence:
>>> firstObject.printValue
<method FirstClass.printValue of FirstClass in-
stance at 0x80db1f4>
This tells you what kind of an object a function is. Calling the function will print
the value of item in your window:
>>> firstObject.printValue()
BlackAdder goes forth
>>>
As I said, the self is supplied by Python, because you call the function from the
object. That is, by prefixing the variable that points to the object to the function
name, with a dot in between. This is the same as typing the following code (that is,
calling the function with the object as its first parameter). As such, the following
two expressions are equivalent:
>>>firstObject.printValue()
BlackAdder goes forth
>>>FirstClass.printValue(firstObject)
BlackAdder goes forth
Of course, typing in all these instructions correctly every time you want the
computer to print BlackAdder goes forth is quite a chore. To get around this, you
can write a small text document (this is not the same as a Word document!) using
BlackAdders text editor, and then ask Python to execute it.
To sum up: composition of complex wholes from smaller parts using a debased
variant of English, and calling things names, is what programming is all about. The
60
Chapter 4. Introduction to Python
rest is made up of rules rules intended to make it easier for computer the compute
to determine what it should do, and more difficult for you to explain yourself to the
machine.
Warning
Please be warned that if you execute your programs (or
scripts) from BlackAdder, all the output of print will disappear
into the void. The output will only be shown if you start your
scripts using the debugger, and have the Python Interpreter
window open. If you merely type in stuff in the Interpreter
window you will see all output.
If this section went over your head with the airspeed of an unladen African swallow,
dont worry. There is much more to programming more than I can explain in a
third of a chapter. Please read the Python tutorial that is included with Python and
with BlackAdder. It is well-written and a little less hasty. Another good source is
the free Livewires Python course, which you can find in PDF format at:
http://www.livewires.org.uk/python/. I heartily recommend it as the best
introduction to the general idea of programming Ive ever read.
61
Chapter 4. Introduction to Python
Objects disappear once the last reference has gone (except if the reference is an
explicit weak reference). You can destroy a reference with del from that
moment on, the name doesnt exist anymore. If you set the reference to None, the
link to the object disappears, but the reference remains in existence.
>>> a="aaa"
>>> print a
aaa
>>> del a
>>> print a
Traceback (most recent call last):
File "<stdin>", line 1, in ?
NameError: name a is not defined
>>> a="aaa"
>>> print a
aaa
>>> a=None
>>> print a
None
>>>
>>> a=A()
>>> id(a)
135121324
Some types are callable (i.e., put on a line with an argument list between ()) and can
return a value. Callable types include classes, methods in clasess, functions and
objects that implement the special method __call__.
4.2.2. Formatting
A block is first marked by a colon at the end of the previous line, and is indented.
The block ends at the next dedent. (You should indent with four spaces, and not use
62
Chapter 4. Introduction to Python
tabs.)
Whatever is typed between brackets is considered to be on one line. Dictionaries are
delimited with curlies {}, lists are delimited with brackets [] and tuples (and lists of
arguments to functions) are delimited with ().
A classname should start with a capital letter; variable and function names should
begin with a lowercase letter.
Only alphabetic characters (a-z, A-Z), digits (0-9) and the underscore (_) are valid
in variable names, but a variable name should not start with a digit.
Names that start with one underscore (_) are a bit private (not imported with from
module import *); names that start with two underscores (__) are very private
in scope (not visible with dir(object)); names that start and end with two
underscores are system-defined.
4.2.3. Keywords
The following keywords are reserved:
63
Chapter 4. Introduction to Python
4.2.4. Literals
Strings can be enclosed in single ( or ") or triple ( or """") quotes. Triple-quoted
strings can span lines, the linebreaks are part of the string. If you prefix the string
literal with u, it becomes a Unicode string.
Numbers can be integers, long integers, floating point, and imaginary. If you divide
integers or long integers, you will not get a float, but the integer before the decimal
symbol (unless you import division from future in Python 2.2).
Python has the following operators:
+ -- * ** / %
<< >> & | ^ ~
< > <= >= == != <>
The comparison operators <> and != are alternate spellings of the same operator. !=
is the preferred spelling; <> is obsolescent.
64
Chapter 4. Introduction to Python
... print a
...
>>> def ff(a, b):
... print a, b
...
>>> def fff(*args):
... print args
...
>>> def ffff(**args):
... print args
...
>>> f(1)
1
>>> ff(1, b=2)
1 2
>>> fff(1,2,3)
(1, 2, 3)
>>> ffff(a=1,b=2,c=3)
{b: 2, c: 3, a: 1}
>>>
>>> t=("a","b","c")
>>> t
(a, b, c)
>>> t[2]="d"
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: object doesnt support item assignment
>>>
65
Chapter 4. Introduction to Python
A list is a list of objects. You can change which objects are in a list, adding and
deleting items to your hearts delight.
A dictiony is a keyed list. Keys, which must be unchangeable (i.e. not lists) point to
values. One key, one value. There can be no duplicate keys in a dictionary.
4.3. Constructions
Python, like all languages, gives you constructions for looping, branching and
jumping. In addition, since Python 2.2, you can also use iterators and generators.
66
Chapter 4. Introduction to Python
4.3.1. Looping
You do not use counters to loop in Python. Rather, you use sequences of objects to
loop over. Those objects can of course also be be numbers, generated by either
range or xrange:
>>> a=0
>>> while a < 3:
... print a
... a+=1
...
0
1
2
The break statement breaks execution out of a loop; the continue statement
continues immediately with the next iteration.
67
Chapter 4. Introduction to Python
# iter.py - an iterator
class MyIterator:
def __iter__(self):
return self
def next(self):
if self.start < 10:
self.start += 1
return self.start
else:
raise StopIteration
for i in MyIterator(1):
print i
Generators are functions that return a function that can yield a result part-way
compution, and resume later:
# generator.py
def MyGenerator():
count = 0
while count < 10:
yield count
count += 1
gen = MyGenerator()
try:
while 1:
print gen.next()
68
Chapter 4. Introduction to Python
except StopIteration:
print "finished"
Note how yield returns the number, but count is still increased.
4.3.2. Branching
The number zero, empty lists, dictionaries, tuples and the object None all evaluate to
false; (almost) everything else is true. You create branches using the if statement.
>>> a=1
>>> if a:
... print "true"
... else:
... print "false"
...
true
>>> if a==0:
... print "a was zero"
... elif a == None:
... print "a was none"
... else:
... print "a was zero nor none"
...
a was zero nor none
The operator == tests for equality, while != (or the deprecated <>) tests for
inequality. The operator is tests for identity: that is, whether two references point
to (unless you import division from future in Python 2.2) the same object:
69
Chapter 4. Introduction to Python
1
>>> a="bla"
>>> b="bla"
>>> a is b
1
>>> id(a)
135455928
>>> id(b)
135455928
As you can see, Python does some optimizations that reuse the same string object if
the string contents are the same.
4.3.3. Exceptions
As every modern programming language must have, Python contains an error
catching construction. This is the try: ... except... construction.
>>> try:
... 1/0
... except ZeroDivisionError:
... print "Zerodivisionerror"
...
Zerodivisionerror
You can also create your own exceptions that can carry significant data about the
causes of the error:
70
Chapter 4. Introduction to Python
...
Bla happened - thats bad!
If you want to catch several different exceptions, you have to create a tuple of all the
exceptions you want to catch:
>>> try:
... print "bla"
... except (ValueError, ZeroDivisionError):
... print "thats bad"
...
bla
Finally, you can define something that should happen when all errors have been
handled in the finally block:
>>> try:
... 1/0
... finally:
... print "finally"
...
finally
Traceback (most recent call last):
File "<stdin>", line 2, in ?
ZeroDivisionError: integer division or modulo by zero
4.3.4. Classes
Classes are defined with the class keyword. Python classes can inherit from zero,
one, or more other classes, but from only one PyQt class.
Classes are initialized using the code in the __init__ method. There are other
special methods, like __str__, which should return a string representation of the
class. Consult the Python language reference for a complete list of these.
>>>class A:pass
71
Chapter 4. Introduction to Python
...
>>> class B(A):
... def __init__(self, val):
... self.val = val
... def __str__(self):
... return str(self.val)
...
>>> b=B(10)
>>> print b
10
>>>
4.4. Conclusion
This concludes a very short tour of Python. There is much more to the language, but
this chapter has described the basis. Its not nearly enough, of course, so please
consult the online documention, which is well-written and reveals all. Furthermore,
think about treating yourself to an introduction like Learning Python.
72
Chapter 5. Debugging
At some point in their career, most programmers realize that their job title should be
"senior debugger" instead of senior developer. Debugging is the art of getting your
code to run as you intended, instead of running as you wrote it. That is the nub,
reallyin most cases its your code that is wrong. Python itself is pretty flawless
there are hardly any noticeable bugs left. The same goes for Qt. PyQt might still
have a few bugs in it, but you would have to be using decidedly advanced features
to stumble onto them. In most cases, your own undocumented features will be
your undoing.
In this chapter well use the debugger included in BlackAdder to find bugs in some
simple scripts. If you dont understand the actual code yet, dont worry you can
always come back later. The main goal is to familiarize yourself with the
BlackAdder environment and the concept of debugging.
There are two basic methods of debugging. The first is sprinkling your code with
print statements that dump the contents of the variables of your application. The
second method is to follow your application as it executes using a good debugger,
examining the application data using the tools the debugger provides.
Python has always been possessed of a basic command-line based debugger, pdb,
similar to the infamous Unix debuggers, dbx and gdb. If youve ever tried to
actually trace an application using one of these, youll know the exact meaning of
the word inconvenient. Using them is on a par with using ed or edlin both line
editors for editing code.
To show a session with pdb:
73
Chapter 5. Debugging
You can see why there have been many attempts to create a useful GUI fronted to
pdb. Most have suffered from the fact that they dont know where to stop debugging.
If you are debugging a piece of code that contains the statement string.join(),
you probably dont want to single-step into the string.py module, which is part
of the Python system libraryand yet this is exactly what happens very often.
BlackAdder includes a very nice debugger, one that knows where to stop
debugging. It includes all the usual facilities, like single-stepping, breakpoints and a
watch panel for variable values.
Currently missing features include conditional breakpoints (a breakpoint that only
breaks execution on certain values for certain variables) and runtime code changes.
You can change variable values runtime using the Python interpreter window,
though.
74
Chapter 5. Debugging
The PyQt library includes another, simpler debugger, called eric. This application is
no longer maintained, so I wont spend much time here describing the way it works.
It does, however, provide a very nice example of using regular expressions and
PyQts QCanvas widget. You can start eric by typing eric on the command-line.
75
Chapter 5. Debugging
The script execution toolbar. From left to right: run script, run project, debug script,
debug project, restart debugger, continue, single step, set breakpoint, clear
breakpoint and cancel debugging.
One thing to be aware of when running scripts or projects from BlackAdder is that
everything you print or write to standard error or standard output gets lost, unless
you have the Python interpreter window active. Eric also prints the output to the
Python shell window.
76
Chapter 5. Debugging
the editor pane. It will place a small, inconspicuous white circle in the editor border.
You can also set and unset breakpoints during a debugging session.
Now, if you start debugging the script, and press the Continue button, the script will
run until it arrives at the print i line. The output will show up in the Python
Interpreter window, if you have it open.
Now that you know how breakpoints work, Ill show a good way to use them.
In GUI programming, breakpoints are often the only way of debugging code that
becomes activated after the main loop has started. Lets look at the following script,
where there is a bug in the code that is activated when the button is pressed:
#
# button.py
#
from qt import *
import sys
class MainWindow(QMainWindow):
self.grid=QGrid(2, self)
77
Chapter 5. Debugging
self.grid.setFrameShape(QFrame.StyledPanel)
self.connect(self.bn, SIGNAL("clicked()"),
self.slotSlot)
self.setCentralWidget(self.grid)
def slotSlot(self):
i = 1/0
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
If you try to single-step until youve arrived at the bug, you will be stepping for a
long time. It is easier to continue the application until it hits the breakpoint in
slotSlot(), and take it from there by hand.
78
Chapter 5. Debugging
# button2.py
#
from qt import *
import sys
class MainWindow(QMainWindow):
self.grid=QGrid(2, self)
self.grid.setFrameShape(QFrame.StyledPanel)
self.connect(self.bn, SIGNAL("clicked()"),
self.slotSlot())
self.setCentralWidget(self.grid)
def slotSlot(self):
i = 1/0
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Running this code wont place the window on screen. Instead, it ends with the
following stacktrace:
79
Chapter 5. Debugging
If you single step this using BlackAdder youll notice that directly after the line:
self.connect(self.bn, SIGNAL("clicked()"),
self.slotSlot())
def slotSlot(self):
Armed with this knowledge, its easy to see what went wrong: we called the
function in the connect line by adding the two brackets, passing the result of the
80
Chapter 5. Debugging
self.connect(self.bn, SIGNAL("clicked()"),
self.slotSlot)
And the bug is fixed! Incidentally, this also shows that you can create new
signal/slot connections on the fly, from the output of a functionthis is something
to be aware of when creating a very dynamic application.
81
Chapter 5. Debugging
In chapter five of the excellent book The Practice of Programming, Kernighan and
Pike advise to try out your hunches with a little bit of code, whenever you wonder
whether something works at all. The Python shell window is tailor made for this:
82
Chapter 5. Debugging
that Ive been developing with Python I have not come across a single bug in
Python, and, whats more, Ive never seen someone post a bug in Python to the
comp.lang.python newsgroup.
Your first course of action should be to subscribe yourself to the PyKDE mailing list
(which also has PyQt as its subject), and check in the archives at
http://mats.gmd.de/mailman/listinfo/pykde, in case your problem has been
mentioned before.
If it hasnt been mentioned, you might want to write the smallest possible script that
reproduces the erroneous behavior. This should be easy enough if you have neatly
modularized your code. You can then post this script together with the debug output
(a stacktrace in technical parlance) to the PyKDE mailing list.
83
Chapter 5. Debugging
84
II. PyQt fundamentals
Table of Contents
6. Qt Concepts ........................................................................................................87
7. Signals and Slots in Depth...............................................................................103
8. String Objects in Python and Qt ....................................................................139
9. Python Objects and Qt Objects ......................................................................159
10. Qt Class Hierarchy.........................................................................................177
11. Qt Designer, BlackAdder and uic .................................................................249
In this part we will take a look at the concepts behind BlackAdder, Python and Qt.
After a short introduction, we will begin an in-depth investigation of the core
concept of PyQt: signals and slots. String handling and object creation are the topics
of the next two chapters. The basic layout of the Qt class library will be explored,
illustrated with trivial and not-so-trivial examples. Finally, we explain how to create
GUI designs using the designer module.
85
Chapter 5. Debugging
86
Chapter 6. Qt Concepts
This chapter describes the way Python and Qt bind together, using the PyQt
wrapper library. Concepts peculiar to PyQt and Qt such as signals and slots,
inheritance and gui design, are introduced by building steadily more complex
versions of the hackneyed hello world example application.
87
Chapter 6. Qt Concepts
QWidget and many other, but not all, Qt classes derive from the QObject base class
a class like QLayout is derived from QObject, but QPixmap is not. Whether a
class is derived from QObject is determined by whether there is a need for signals
and slots, and whether the created objects must be placed in an ownership hierarchy.
Scripting languages like Python have a reputation for bad performance, but PyQt
applications perform very well indeed; there is just a thin wrapper between the GUI
objects and Python. Those GUI objects do most of the heavy work of pixel shifting,
and they are written in well-optimized C++. If you try to do things like writing your
own DTP layout engine from scratch using the Qt drawing primitives, you might be
hindered by the slowness inherent in a byte-code interpreted language like Python,
but on the whole, your application will be as responsive as one written in pure C++,
and youll have a working application where you would still be hacking the first
prototype in C++.
88
Chapter 6. Qt Concepts
an application object
a main window (which has a central widget), or
a main widget
This is the traditional Hello World button application, with as little code as
possible:
Hello World
#
# hello1.py
#
import sys
from qt import *
app=QApplication(sys.argv)
button=QPushButton("Hello World", None)
app.setMainWidget(button)
button.show()
app.exec_loop()
We need to import the Python sys package, because the QApplication object
wants to look at the command-line arguments the script is started with. For
instance, starting the script with python hello1.py
-style=platinum starts the script with the look and feel of Mac-OS 8.5, by
passing the -style=platinum option through to the QApplication object.
89
Chapter 6. Qt Concepts
One of the niceties of Qt is that you have access to all supported widget styles
on all platforms. (Except for the Aqua style - that is only available on OS X,
because Apple doesnt want it to spread to other platforms.)
Next, we have to import the qt library. While it is possible to import only
explicitly the elements from the library we need, its just as easy and efficient to
import the whole library. But we could have said:
from qt import QApplication,
QPushButton
From version 3.x of PyQt, the library has been split into several separate
modules. The Qt module still gets you all the basic stuff, but more advanced
functionality, such as the canvas, is divided over separate modules, qtcanvas
for QCanvas, for instance.
After importing the necessary modules, we create a Qt application object. This
object handles the dispatching of events from the mouse and keyboard to the
various widgets of the application. Never try to create more than one
QApplication object, not even if you embed Python and PyQt in a C++ Qt
application. In that case, the C++ application should create the QApplication
object and pass a reference to the embedded Python interpreter.
To keep things simple, we do not create a separate window object, but rather
simply a pushbutton, of the type QPushButton. The first argument to the
creation of the QPushButton is the text that is shown on the button. Since this
is the only widget of the application, it doesnt have a parent. This is what the
None argument means there is no parent, and the QPushButton is the root
of the application widget tree.
However, we still need to apprise the QApplication object of that fact this
is done by telling the QApplication that our button is the main widget:
app.setMainWidget(button)
90
Chapter 6. Qt Concepts
Note: Note that this is one of the few instances where a method name differs
between Python and C++: the C++ method is called exec() , which is a
reserved word in Python. Except for a few cases like this, reading the C++
documentation for Python use demands little more than a simple mental
substitution.
Experienced Pythoneers will also note that the parameters in PyQt function
calls are positional not by keyword. In the old Tkinter GUI toolkit most
function calls take the form:
Just something to be aware of: keyword parameters can be added in any old
order, but positional parameters have to be in the right position.
import sys
from qt import *
class HelloButton(QPushButton):
91
Chapter 6. Qt Concepts
class HelloWindow(QMainWindow):
def main(args):
app=QApplication(args)
win=HelloWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
This is more like it! While still boring and trivial, this small program shows several
important aspects of programming with Python and Qt: the subclassing of Qt
classes in Python, the use of windows and widgets, and the use of signals and slots.
In most PyQt applications you will create a custom main window class, based on
QMainWindow, and at least one custom main view widget, based on any Qt widget
it could be a listview, an editor window or a canvas, or, as in this case, a simple
button. Although PyQt allows you to subclass almost any Qt class, you cant base a
Python class on more than one Qt class at a time.
That is, multiple inheritance of Qt classes is not supported. This is seldom (if ever)
a problemtry to imagine what a widget that looks like a checkbox and a
radiobutton at the same time. Using two widgets in one custom widgets is another
matter, called delegation, and is fully supported.
In this script we have subclassed QMainWindow to create a custom window that
contains a pushbutton as its central widget. Almost always, a window will have the
usual frills around the borders menus, toolbars and statusbars. This is what
92
Chapter 6. Qt Concepts
QMainWindow is designed for. We didnt define any menu items, so the window is
still a bit bare.
The central part of a windowthe letterbox, so to speakis where the
application-specific functionality appears. This is, of course, our button.
QMainWindow manages the resizing of its central widget automatically, as you
might have noticed when dragging the borders of the window. Also, note the
difference in geometry between this version of Hello World and the previous one:
this is caused by the automatic layout handling that QMainWindow provides.
You set the central part of the window with the setCentralWidget() method:
self.setCentralWidget(self.button)
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
93
Chapter 6. Qt Concepts
Lets analyze this line: the app object makes a connection between a signal
lastWindowClosed() (which is sent by the application object itself), and its own
quit() function. Using signals and slots from Python is extremely convenient,
both for gui work and in more abstract situations where a decoupling between
objects is desirable.
Another example of using signals and slots is in the following rewrite of the
HelloWindow class:
...
class HelloWindow(QMainWindow):
We have added a line where the clicked() signal, which is emitted by the
QPushButton when it is clicked, is connected to the close() slot of the
HelloWindow class. Since HelloWindow inherits QMainWindow, it also inherits
all its slot functions.
Now, if you click on the button, the window closesand we have our first
interactive PyQt application!
An interesting exercise is to create more than one window by rewriting the main
function:
...
def main(args):
app=QApplication(args)
winlist=[]
for i in range(10):
win=HelloWindow()
94
Chapter 6. Qt Concepts
win.show()
winlist.append(win)
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
...
If you run this version of the script, ten windows will rapidly pop up on your
desktop. You can close each window by pressing either the button or using the
window controls the application will only stop when the last one is closed.
Try commenting out the line winlist.append(win) :
...
def main(args):
app=QApplication(args)
winlist=[]
for i in range(10):
win=HelloWindow()
win.show()
#winlist.append(win)
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
...
95
Chapter 6. Qt Concepts
difference, as C++ does not delete unused objects for you, which can easily lead to
nasty memory leaks.
96
Chapter 6. Qt Concepts
Its quite easy to work with designer just keep in mind that you never have to
place items pixel-perfect. Just bang widgets of roughly the right size in roughly the
right places, add the Qt layout managers, and see them work their magic.
You add a layout manager by selecting the widgets you want to be managed, and
then selecting the right layout manager from the toolbar.
In the above design, there are three layout managers: the buttons on the right are
stacked, the widgets inside the bevel are in a grid, and everything in the form is in
another grid. Try making the dialog larger and smaller it will always look good.
Even better, if a visually impaired user chooses a large system font, say Arial 24
points bold, the form will still look good.
You can either compile the .ui file to Python code from BlackAdder or from the
command-line. The result will be something like this:
from qt import *
class frmConnect(QDialog):
def __init__(self,parent = None,name = None,modal = 0,fl = 0):
QDialog.__init__(self,parent,name,modal,fl)
if name == None:
self.setName(frmConnect)
self.resize(547,140)
self.setCaption(self.tr(Connecting))
97
Chapter 6. Qt Concepts
self.setSizeGripEnabled(1)
frmConnectLayout = QGridLayout(self)
frmConnectLayout.setSpacing(6)
frmConnectLayout.setMargin(11)
Layout5 = QVBoxLayout()
Layout5.setSpacing(6)
Layout5.setMargin(0)
self.buttonOk = QPushButton(self,buttonOk)
self.buttonOk.setText(self.tr(&OK))
self.buttonOk.setAutoDefault(1)
self.buttonOk.setDefault(1)
Layout5.addWidget(self.buttonOk)
self.buttonCancel = QPushButton(self,buttonCancel)
self.buttonCancel.setText(self.tr(&Cancel))
self.buttonCancel.setAutoDefault(1)
Layout5.addWidget(self.buttonCancel)
self.buttonHelp = QPushButton(self,buttonHelp)
self.buttonHelp.setText(self.tr(&Help))
self.buttonHelp.setAutoDefault(1)
Layout5.addWidget(self.buttonHelp)
spacer = QSpacerItem(20,20,QSizePolicy.Minimum,QSizePolicy.Expandi
Layout5.addItem(spacer)
frmConnectLayout.addLayout(Layout5,0,1)
self.grpConnection = QGroupBox(self,grpConnection)
self.grpConnection.setSizePolicy(QSizePolicy(5,7,self.grpConnectio
sizePolicy().hasHeightForWidth()))
self.grpConnection.setTitle(self.tr())
self.grpConnection.setColumnLayout(0,Qt.Vertical)
self.grpConnection.layout().setSpacing(0)
self.grpConnection.layout().setMargin(0)
grpConnectionLay-
out = QGridLayout(self.grpConnection.layout())
grpConnectionLayout.setAlignment(Qt.AlignTop)
grpConnectionLayout.setSpacing(6)
98
Chapter 6. Qt Concepts
grpConnectionLayout.setMargin(11)
self.lblName = QLabel(self.grpConnection,lblName)
self.lblName.setText(self.tr(&Name))
grpConnectionLayout.addWidget(self.lblName,0,0)
self.lblHost = QLabel(self.grpConnection,lblHost)
self.lblHost.setText(self.tr(&Host))
grpConnectionLayout.addWidget(self.lblHost,2,0)
self.lblPasswd = QLabel(self.grpConnection,lblPasswd)
self.lblPasswd.setText(self.tr(&Password))
grpConnectionLayout.addWidget(self.lblPasswd,1,0)
self.txtPasswd = QLineEdit(self.grpConnection,txtPasswd)
self.txtPasswd.setMaxLength(8)
self.txtPasswd.setEchoMode(QLineEdit.Password)
grpConnectionLayout.addWidget(self.txtPasswd,1,1)
self.cmbHostnames = QComboBox(0,self.grpConnection,cmbHostnames)
grpConnectionLayout.addWidget(self.cmbHostnames,2,1)
self.txtName = QLineEdit(self.grpConnection,txtName)
self.txtName.setMaxLength(8)
grpConnectionLayout.addWidget(self.txtName,0,1)
frmConnectLayout.addWidget(self.grpConnection,0,0)
self.connect(self.buttonOk,SIGNAL(clicked()),self,SLOT(accept()
self.connect(self.buttonCancel,SIGNAL(clicked()), \
self,SLOT(reject()))
self.setTabOrder(self.txtName,self.txtPasswd)
self.setTabOrder(self.txtPasswd,self.cmbHostnames)
self.setTabOrder(self.cmbHostnames,self.buttonOk)
99
Chapter 6. Qt Concepts
self.setTabOrder(self.buttonOk,self.buttonCancel)
self.setTabOrder(self.buttonCancel,self.buttonHelp)
self.lblName.setBuddy(self.txtName)
self.lblPasswd.setBuddy(self.txtName)
Now this looks pretty hideous but fortunately youll never have to hack it. You
would lose all your changes anyway, the next time you make a change to your
design and regenerate the Python code. The best thing to do is to subclass this form
with code that actually fills the dialog with data and perfoms an action upon closing
it. I like to keep the names of the generated form and the subclassed form related,
and I tend to refer to the first as a form, and the second as dialog hence the prefix
frmXXX for generated forms and dlgXXX for the dialogs.
For example:
import sys
from qt import *
class dlgConnect(frmConnect):
def accept(self):
print self.txtName.text()
print self.txtPasswd.text()
print self.cmbHostnames.currentText()
frmConnect.accept(self)
if __name__ == __main__:
app = QApplication(sys.argv)
QObject.connect(app, SIGNAL(lastWindowClosed()),
100
Chapter 6. Qt Concepts
app, SLOT(quit()))
win = dlgConnect()
app.setMainWidget(win)
win.show()
app.exec_loop()
As you can see, we have subclassed the generated form. In the constructor, the
various fields are filled with a bit of data. Note that we can simply use Python string
objects in setText() methods. Qt uses a special string object, QString for all its
textual data, but PyQt automatically translates both Python strings and Python
unicode strings to these QString objects. There are some complications, which we
deal with in Chapter 8, but the translation is mostly transparent.
When you press the OK button, Qt calls the accept() method of the dialog class,
in this case dlgConnect, which inherits frmConnect, which inherits QDialog.
The accept() method prints out the contents of the fields. Then the accept()
method of the parent class ultimately QDialog is called, and the dialog is
closed.
6.5. Conclusion
In this chapter weve taken a first sip of developing with Python and PyQt, touching
lightly on many subjects. In the rest of this part, well investigate these issues in
depth. Then we will start building real software in Part III. If you get lost, you can
always refer to the online documentation that comes with Qt or BlackAdder.
Appendix A, Reading the Qt Documentation, tells you how you can read the C++
class documentation with profit and pleasure.
101
Chapter 6. Qt Concepts
102
Chapter 7. Signals and Slots in Depth
The concept of signals and slots is possibly the most interesting innovation in the Qt
library. Good widgets and a clean API are rare, but not unique. But until Qt
appeared on the horizon, connecting your widgets with your application and your
data was a nasty and error-prone endeavor even in Python. I will first discuss the
problem that is solved by signals and slots in some detail; then I will introduce the
actual mechanics of the signal/slot mechanism, and finish with an application of the
technique outside the GUI domain.
#
# stupid_button.py -- this button is not reusable
#
class Button:
def clicked(self):
self.application.doSomeApplicationSpecificFunction()
class Application:
def __init__(self):
self.button=Button(self)
103
Chapter 7. Signals and Slots in Depth
def doSomeApplicationSpecificFunction(self):
print "Function called"
app=Application()
app.button.clicked() # simulate a user button press
7.1.1. Callbacks
This is no solution the button code isnt reusable at all. A better solution would
be to pass the function object to the button. Remember that in Python functions are
objects just like everything else. In a language like C or C++ you would pass a
function pointer, the actual memory address of the function being called. This is
quite nasty, because there is no way the compiler can check what arguments are
passed to the function represented by the function pointer. In Python, passing
functions around is really easy.
#
# callback.py -- handing the function over the the app
#
class Button:
def clicked(self):
apply(self.callbackFunction)
class Application:
def __init__(self):
self.button=Button(self.doSomeApplicationSpecificFunction)
def doSomeApplicationSpecificFunction(self):
print "Function called"
104
Chapter 7. Signals and Slots in Depth
app=Application()
app.button.clicked() # simulate a user button press
Using apply() to execute function objects.: Note the usage of the apply()
function in the clicked() function this Python built-in function executes the
function object you pass as the first argument argument. You can also hand it
parameters, as a tuple in the second argument to apply() . Youll see that
idiom quite often when we subclass Qt classes:
class MyWidget(QWidget):
This is useful because QWidget and the other Qt classes often have a lot of
optional parameters, such as the object name or certain widget flags. If we
discount the possibility that someone wants to use those optional parameters,
we would write:
class MyWidget(QWidget):
This is far less flexible. In the previous example, we created an argument tuple
to be passed to the __init__() by first creating a tuple containing our own
object reference - self, and then adding the arguments from the variable
positional argument list to that tuple. Remember from the discussion of
positional arguments in Section 4.2.5 that the arguments in *args are a tuple,
and you can create a new tuple by adding two tuples.
In more recent versions of Python, you dont need to use apply() anymore to
call the constructor of a superclass with a variable number of arguments. That
is, from version 2.0 of Python you can also use the following construction:
105
Chapter 7. Signals and Slots in Depth
>>> a=O()
>>> b=O(a, "bla")
>>> b
<__main__.O instance at 0x82b5c3c>
>>> b.name()
bla
>>> b.parent()
<__main__.O instance at 0x8106cb4>
>>>
That is, when calling the constructor of the superclass, you can pass self as
the first argument, and then the argument list, with asterisks and all.
#
# registry.py -- a central registry of connected widgets
#
class Registry:
def __init__(self):
self.connections={}
106
Chapter 7. Signals and Slots in Depth
registry=Registry()
class Button:
def clicked(self):
registry.execute("clicked")
class Application:
def __init__(self):
self.button=Button()
registry.add("clicked", self.doAppSpecificFunction)
registry.add("clicked", self.doSecondFunction)
def doAppSpecificFunction(self):
print "Function called"
def doSecondFunction(self):
print "A second function is called."
app=Application()
app.button.clicked()
107
Chapter 7. Signals and Slots in Depth
The actual registry is a Python dictionary with the name connections. Here,
each occasion is used as a key to find the actual function object that should be
called.
If the occasion is already registered, we simply add a new entry to the list;
otherwise a new entry is created in the registry.
If the occasion exists, then we remove the relevant function entry from its list
of functions.
We loop over all functions that belong to this occasion and simply execute
them by calling apply() on them.
A registry is a unique object to an application: there should only be one, so we
create it globally.
This is the button class. Whenever the button is clicked, it calls the
execute() function in the registry with the clicked occasion.
The application creates one button and binds two of its functions to the button.
This looks a lot like the way connections are made in Qt!
Here we simulate a button click by directly calling the clicked() function on
the button.
This is one step up from the previous example, which was an extremely crude
implementation of the well known Observer design pattern, in that there is now a
neutral object that mediates between the button and the application. However, it is
still not particularly sophisticated. It certainly wouldnt do for a real application
where there might be many objects with the same occasion.
It is quite possible to implement a solution like this in pure Python, especially with
the weak references module that debuted in Python 2.1. Bernhard Herzog has done
so in his fine Python application Sketch (http://sketch.sourceforge.net). He had to
do it himself because he was working in PyGTK, not PyQt. Fortunately, PyQt
has already solved the whole problem for us.
108
Chapter 7. Signals and Slots in Depth
Signals, messages, events: This is one area where there is a perfect Babel
of tongues. Even really knowledgeable people like Dr Dobbs Al Stevens get
confused when confronted with terms like message, event or signal.
In PyQt programming, the term message is quite irrelevant it is used in
Windows programming to indicate function calls made from your application to
the Windows GUI libraries.
Events and signals, on the other hand, are central to PyQt. Signals and slots
are used to connect one object to another. An example is the perennial
pushbutton, whose clicked() signal gets connected to the accept() slot
function of a dialog box. Signals are used to connect entities internal to the
application.
Events are more often generated directly by user input, such as moving or
clicking with the mouse, or typing at the keyboard. As such, they dont connect
two class instances, but rather a physical object, such as a keyboard, with an
109
Chapter 7. Signals and Slots in Depth
application. Events are encapsulated by the QEvent class, and are mostly
delivered to QWidget and its descendants. Events are used to communication
with external entities.
110
Chapter 7. Signals and Slots in Depth
PyQt defines three special functions that appear to be macros (because of their
all-caps spelling, as in C++) but are in fact just functions. (In fact, there are no
macros in Python). These are SLOT(), SIGNAL() and PYSIGNAL().
Two of these functions are meant for signals and slots defined in C++; the other is
meant for signals defined in Python. Signals and slots defined in C++ are connected
on the level of C++ (i.e., not in the sip registry) and can be a bit faster.
The first function is SLOT(), which marks its only argument, a string, as a slot
defined in the Qt library, i.e. in C++. The corresponding SIGNAL, which also has
one string argument, marks its argument as a signal as defined in Qt.
For instance, from the documentation of QListview we can learn that this class
possesses the slot invertSelection() . From the documentation of QButton we
learn that it can emit a signal clicked(). We can connect a button press to this slot
as follows:
#
# lsv.py - connect a button to a listview
#
import sys
from qt import *
class MainWindow(QMainWindow):
self.mainWidget=QWidget(self);
self.lsv = QListView(self.mainWidget)
self.lsv.addColumn("First column")
self.lsv.setSelectionMode(QListView.Multi)
self.lsv.insertItem(QListViewItem(self.lsv, "One"))
self.lsv.insertItem(QListViewItem(self.lsv, "Two"))
self.lsv.insertItem(QListViewItem(self.lsv, "Three"))
111
Chapter 7. Signals and Slots in Depth
self.vlayout.addWidget(self.lsv)
self.vlayout.addWidget(self.bn)
QObject.connect(self.bn, SIGNAL("clicked()"),
self.lsv, SLOT("invertSelection()"))
self.setCentralWidget(self.mainWidget)
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
You dont need to keep self references to the widgets, because these widgets
are child objects to QMainWindow. However, if you later want to access those
widgets, it is necessary to have a reference.
The QListView is a child widget to the mainWidget. It has one column and
owns three listview items. In order to give the pushbutton some useful work to
do, we allow a multiple selection.
A very standard pushbutton nothing special, except that is is a child of the
mainWidget.
112
Chapter 7. Signals and Slots in Depth
This is the actual connection between the clicked() signal of the button and
the invertSelection() of the listview. If you press the button, youll notice
the effect.
Note that the arguments of SIGNAL and SLOT are used as an index of the
dictionary sip keeps of available slots and signals, and that you should match the
definition of the signal and slot as given in the class documentation exactly.
A more complicated signal/slot combination can pass an integer along (or even a
complete object). Lets connect the knob of a QDial to a few functions, creating an
color dialer. A QDial generates the valueChanged(int) signal, which passes the
current value of the dial in the form of an integer to every slot thats connected to
the signal. You need to explicitly enter the types of the signal arguments, but not
their names.
#
# dial.py -- connecting a QDial to a QLabel or two
#
import sys
from qt import *
class MainWindow(QMainWindow):
self.red = 0
self.green = 0
self.blue = 0
113
Chapter 7. Signals and Slots in Depth
self.dialRed.setBackgroundColor(QColor("red"))
self.dialRed.setNotchesVisible(1)
self.dialGreen = QDial(0, 255, 1, 0, self)
self.dialGreen.setBackgroundColor(QColor("green"))
self.dialGreen.setNotchesVisible(1)
self.dialBlue = QDial(0, 255, 1, 0, self)
self.dialBlue.setBackgroundColor(QColor("blue"))
self.dialBlue.setNotchesVisible(1)
self.hlayout.addWidget(self.dialRed)
self.hlayout.addWidget(self.dialGreen)
self.hlayout.addWidget(self.dialBlue)
self.vlayout.addLayout(self.hlayout)
self.labelLayout.addWidget(self.labelRed)
self.labelLayout.addWidget(self.labelGreen)
self.labelLayout.addWidget(self.labelBlue)
self.vlayout.addLayout(self.labelLayout)
QOb-
ject.connect(self.dialRed, SIGNAL("valueChanged(int)"),
self.slotSetRed)
QOb-
ject.connect(self.dialGreen, SIGNAL("valueChanged(int)"),
self.slotSetGreen)
QOb-
ject.connect(self.dialBlue, SIGNAL("valueChanged(int)"),
self.slotSetBlue)
QOb-
ject.connect(self.dialRed, SIGNAL("valueChanged(int)"),
self.slotSetColor)
QOb-
ject.connect(self.dialGreen, SIGNAL("valueChanged(int)"),
self.slotSetColor)
114
Chapter 7. Signals and Slots in Depth
QOb-
ject.connect(self.dialBlue, SIGNAL("valueChanged(int)"),
self.slotSetColor)
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Note that we connect the C++ signals (SIGNAL), to Python functions. You simply
give the function object as the slot argument not the result of the function call.
Consider the difference between:
QObject.connect(self.dialBlue,
115
Chapter 7. Signals and Slots in Depth
SIGNAL("valueChange(int)"),
self.slotSetColor())
QObject.connect(self.dialBlue,
SIGNAL("valueChange(int)"),
self.slotSetColor)
which is right. All that difference for two little brackets! This is a rather frequent
typo or thinko. (However, to give you a glimpse of the dynamic nature of Python, if
you have a function that returns the correct function to connect to the signal, you do
want a function call in connect().)
Note also that the number and type of arguments of the signal and the slot you want
to connect have to match. When connecting C++ signals to C++ slots, there is also a
bit of type-checking done.
Python signals are indicated by the PYSIGNAL() function, which also takes a
string. There is no PYSLOT() function corresponding to SLOT(), because you can
use any function as a slot in Python.
The argument of PYSIGNAL() is a simple string that is unique for the class from
which the signal is emitted. It performs the same function as the occasion string in
the small registry.py script. The difference is that PYSIGNAL() string needs to
be unique only for the class, and not the whole application.
Connecting to a Python signal doesnt differ much from connecting to a C++ signal,
except that you dont have to worry so much about the type and number of
arguments of the signal. To rewrite the registry.py example:
#
# sigslot.py -- python signals and slots
#
from qt import *
class Button(QObject):
116
Chapter 7. Signals and Slots in Depth
def clicked(self):
self.emit(PYSIGNAL("sigClicked"), ())
class Application(QObject):
def __init__(self):
QObject.__init__(self)
self.button=Button()
self.connect(self.button, PYSIGNAL("sigClicked"),
self.doAppSpecificFunction)
self.connect(self.button, PYSIGNAL("sigClicked"),
self.doSecondFunction)
def doAppSpecificFunction(self):
print "Function called"
def doSecondFunction(self):
print "A second function is called."
app=Application()
app.button.clicked()
Running this example from the command line gives the following output:
The Button emits the Python signal. Note the construction: the second argument to
the emit function is a tuple that contains the arguments you want to pass on. It must
always be a tuple, even if it has to be an empty tuple, or a tuple with only one
element. This is shown in the next example, in which we have to explicitly create an
empty tuple, and a tuple with one element from a single argument, by enclosing the
argument in brackets and adding a comma:
117
Chapter 7. Signals and Slots in Depth
#
# sigslot2.py -- python signals and slots with arguments
#
from qt import *
class Widget(QObject):
def noArgument(self):
self.emit(PYSIGNAL("sigNoArgument"), ())
def oneArgument(self):
self.emit(PYSIGNAL("sigOneArgument"), (1, ))
def twoArguments(self):
self.emit(PYSIGNAL("sigTwoArguments"), (1, "two"))
class Application(QObject):
def __init__(self):
QObject.__init__(self)
self.widget = Widget()
self.connect(self.widget, PYSIGNAL("sigNoArgument"),
self.printNothing)
self.connect(self.widget, PYSIGNAL("sigOneArgument"),
self.printOneArgument)
self.connect(self.widget, PYSIGNAL("sigTwoArguments"),
self.printTwoArguments)
self.connect(self.widget, PYSIGNAL("sigTwoArguments"),
self.printVariableNumberOfArguments)
def printNothing(self):
print "No arguments"
118
Chapter 7. Signals and Slots in Depth
app=Application()
app.widget.noArgument()
app.widget.oneArgument()
app.widget.twoArguments()
Note the usage of the *arg argument definition. This Python construct means that a
variable length list of un-named arguments can be passed to a function. Thus
printVariableNumberOfArguments(self, *args) fits every signal that you
care to connect it to.
Its an interesting test to run this script several times: you will notice that the order
in which the signals generated by twoArguments() arrive at their destination is
not fixed. This means that if a signal is connected to two or more slots, the slots are
not called in any particular order. However, if two signals are connected to two
separate slots, then the slots are called in the order in which the signals are emitted.
The following combinations of arguments to the connect() function are possible:
119
Chapter 7. Signals and Slots in Depth
7.3. Disconnecting
What can be bound, can be severed, and even for signals and slots there are divorce
120
Chapter 7. Signals and Slots in Depth
121
Chapter 7. Signals and Slots in Depth
which will be called whenever a new item is selected in the datasource combobox.
Drawing a simple line from the combobox to the form gives us the opportunity to
connect signal and slot:
#
# datasource.py -- a monitor for different datasources
#
122
Chapter 7. Signals and Slots in Depth
The sys module is needed for QApplication; whrandom is one of the two
random modules Python provides.
def randomFunction():
return str(whrandom.randrange(0, 100))
def timeFunction():
return ctime(time())
def cookieFunction():
123
Chapter 7. Signals and Slots in Depth
A list of pithy quotes global to this script, so we can treat it like a kind of
constant.
We will define three functions that provide some data. Later on, theres a
generic DataSource class that can use one of these functions to compute some
data. This function, obviously, generates random numbers.
There is no real, practical reason to choose the whrandom module over the
random module. The randrange(start, end, step) function returns a
random integer between start and end. Note that we let this function return a
string, not a number. All data produced by the datasource should be in the same
format.
This function will simply produce the current date and time.
The time() gives the the number of seconds elapsed since the epoch what
that means is OS-dependent. For Unix, its January 1, 1970. The ctime()
converts that to nice text.
This last function will return a cookie, one of the COOKIES list.
Note how we use whrandom.randrange() here to pick one from a list the
start of the range is 0, the length is the length of the cookies list.
class DataSource(QObject):
124
Chapter 7. Signals and Slots in Depth
Every second (1000 milliseconds) the timer will generate an event that will be
caught by the timerEvent function.
By creating a local name that links to the passed function object, we can call
this function as if it were a plain member function of the class.
The timerEvent is called every second because of the events generated by the
timer object.
A Python signal is emitted, of the name "timeSignal" which passes the result of
the dataFunction on.
class DataWindow(frmDataSource):
self.sources = {
"random" : DataSource(randomFunction),
"time" : DataSource(timeFunction),
"cookies" : DataSource(cookieFunction)
}
self.cmbSource.insertStrList(self.sources.keys())
self.currentSource=self.sources.keys()[0]
self.connect(self.sources[self.currentSource],
PYSIGNAL("timeSignal"),
self.appendData)
125
Chapter 7. Signals and Slots in Depth
def append-
Data(self, value): (10)
self.mleWindow.insertLine(value)
self.mleWindow.setCursorPosition(self.mleWindow.numLines(), 0)
126
Chapter 7. Signals and Slots in Depth
The variable passed by the signal connected to this slot is of the QString type.
The index to the dictionary of data sources is a Python string. This is one
instance where we must convert a QString to a Python string.
Using the cached current datasource, we disconnect the signals it generates from
the appendData function.
After the signal is disconnected, we can create a new connection.
(10)This is the function that shows the data. It simply adds every value that is
passed on by the signal to the multi-line edit widget, and then sets the cursor to
the last line. If this is not done, the display will not follow the added data, and
instead stay at the beginning.
def main(args):
a = QApplication(args)
QObject.connect(a,SIGNAL(lastWindowClosed()),a,SLOT(quit()))
w = DataWindow()
a.setMainWidget(w)
w.show()
a.exec_loop()
if __name__ == __main__:
main(sys.argv)
As you can see, connecting and disconnecting signals and slots is a natural and
intuitive technique. Their use is not limited to connecting GUI widgets, as signals
and slots are also useful for the separation of the data model of an application from
its interface. In Part III, We will investigate an application model based on the strict
separation of model and interface, using signals and slots to tie everything together.
127
Chapter 7. Signals and Slots in Depth
widgets with each other, and most of your slot implementations will be in
subclasses of QWidget but the mechanism works well under other
circumstances. A GUI is not necessary.
In this section, I will show how signals and slots make a natural extension to the
event driven nature of XML parsers. As you probably know, XML is a fairly simple
mark-up language that can be used to represent hierarchical data. There are
basically two ways to look at XML data. One is to convert the data in one fell
swoop into some hierarchical representation (for example, dictionaries containing
dictionaries). This method is the DOM (data-object-model) representation.
Alternatively, you can parse the data character by character, generating an event
every time a certain chunk has been completed; this is the SAX parser model.
Python contains support for both XML handling models in its standard libraries.
The currently appreciated module is xml.sax, which can make use of the fast expat
parser. However, expat is not part of standard Python. There is an older, deprecated
module, xmllib, which uses regular expressions for parsing. While deprecated, this
module is still the most convenient introduction to XML handling with Python. Its
also far more Pythonic in feel than the Sax module, which is based on the way
Java does things.
Well create a special module that will use xmllib to parse an XML document and
generate PyQt signals for all elements of that document. It is easy to connect these
signals to another object (for instance, a PyQt QListView which can show the
XML document in a treeview). But it would be just as easy to create a formatter
object that would present the data as HTML. A slightly more complicated task
would be to create a formatter object that would apply XSLT transformations to the
XML document that is, it would format the XML using stylesheets. Using
signals and slots, you can connect more than one transformation to the same run of
the parser. A good example would be a combination of a GUI interface, a validator,
and a statistics calculator.
The next example is very simple. It is easy to extend, though, with special nodes for
comments, a warning message box for errors, and more columns for attributes.
128
Chapter 7. Signals and Slots in Depth
import sys
import xmllib
from qt import *
TRUE=1
FALSE=0
It is often convenient to define constants for the boolean values true and false.
class Parser(xmllib.XMLParser):
129
Chapter 7. Signals and Slots in Depth
This is the Parser class. It inherits the XMLParser class from the xmllib
module. The XMLParser class can be used in two ways: by overriding a set of
special methods that are called when the parser encounters a certain kind of
XML element, or by overriding a variable, self.elements, which refers to a
dictionary of tag-to-method mappings. Overriding self.elements is very
helpful if you are writing a parser for a certain DTD or XML document type
definition, though it is not the way to go for a generic XML structure viewer
(such as the one we are making now).
An example for a Designer ui file could contain the following definition:
self.elements={widget : (self.start_widget,
self.end_widget)
,class : (self.start_class,
self.end_class)
,property: (self.start_property,
self.end_property)
,name : (self.start_name,
self.end_name)}
The keys to this dictionary are the actual tag strings. The tuple that follows the
key consists of the functions that should be called for the opening and the
ending tag. If you dont want a function to be called, enter None. Of course, you
must implement these functions yourself, in the derived parser class.
The first argument (after self, of course) to the constructor is a QObject.
Multiple inheritance isnt a problem in Python, generally speaking, but you
cannot multiply inherit from PyQt classes. Sip gets hopelessly confused if you
do so. So we pass a QObject to the constructor of the Parser class. Later, we
will have this QObject object emit the necessary signals.
The start function takes a string as its parameter. This string should contain
the entire XML document. It is also possible to rewrite this function to read a
file line by line; the default approach makes it difficult to work with really large
XML files. Reading a file line by line is a lot easier on your computers memory.
You should call close() after the last bit of text has been passed to the parser.
130
Chapter 7. Signals and Slots in Depth
#
# Data handling functions
#
def handle_xml(self, encoding, standalone):
self.qObject.emit(PYSIGNAL("sigXML"),
(encoding, standalone))
def handle_doctype(self, tag, pubid, syslit, data):
self.qObject.emit(PYSIGNAL("sigDocType"),
(tag, pubid, syslit, data,))
def han-
dle_special(self, data): (10)
self.qObject.emit(PYSIGNAL("sigSpecial"), (data,))
131
Chapter 7. Signals and Slots in Depth
(tag,attributes))
(13)
def unknown_endtag(self, tag):
self.qObject.emit(PYSIGNAL("sigEndTag"),(tag,))
(14)
def unknown_charref(self, ref):
self.qObject.emit(PYSIGNAL("sigCharRef"),(ref,))
132
Chapter 7. Signals and Slots in Depth
There can be data in between the tags in an XML document, just as with the text
in a HTML document. This function is called when the parser encounters such
data.
In XML, you can use special characters that are entered with &#, a number, and
closed with a semicolon. Pythons xmllib will want to translate this to an ASCII
character. You cannot use xmllib to parse documents that contain references to
Unicode characters.
XML has the same kind of comments as HTML. Most parsers simply pass the
comments, but if you want to show them (for instance, in a structured view of an
XML document) or if you want to preserve the contents of the file exactly, you
can connect a slot to the signal emitted by this function.
CDATA is literal data enclosed between <![CDATA[ and ]]>. A file containing
<![CDATA[surely you will be allowed to
starve to death in one of the royal parks.]]>
will present the quote surely you will be allowed to starve to death in one of the
royal parks. to any slot that is connected to sigCData.
This is called when the XML document contains processing instructions. A
processing instruction begins with <?. All special cases, such as the XML
declaration itself, are handled by other methods.
You can declare entities in XML references to something externally defined.
Those start with <!. The contents of the declaration will be passed on in the
data argument.
(10)XML is far less forgiving than HTML (or at least, XML has both a stricter
definition and less easy-going parsers), and whenever an error is encountered,
such as forgetting to close a tag, this method is called.
(11) unknown_starttag is the most interesting method in the
xmllib.XMLParser class. This is called whenever the xmllib parser
encounters a plain tag that is not present in its elements dictionary. That is, it
will be called for all elements in our current implementation.
(12)Likewise, unknown_endtag is called for the corresponding ending tags.
133
Chapter 7. Signals and Slots in Depth
The TreeView class will show the contents of the XML file.
class TreeView(QListView):
134
Chapter 7. Signals and Slots in Depth
Because XML is a hierarchical file format, elements are neatly nested in each
other. In order to be able to create the right treeview, we should keep a stack of
the current element depth. The last element of the stack will be the parent
element of all new elements.
This option sets the beginning of the tree at the first element, making it clear to
the user that its an expandable tree instead of a simple list.
We present only one column in the listview if you want to show the attributes
of elements, too, you might add a few more columns.
The startDocument function is called when the XML document is opened. It
also starts the call stack by creating the first element. The first QListViewItem
object has the listview as a parent; all others with have a QListViewItem
object as parent. The constructor of QListViewItem is so overloaded that sip
tends to get confused, so I create the item and set its text separately.
Whenever an element is opened, a QListViewItem item is created and pushed
on the stack, where it becomes the parent for newly opened elements.
Conversely, when the element is closed, it is popped from the stack.
def main(args):
if (len(args) == 2):
app = QApplication(sys.argv)
QObject.connect(app, SIGNAL(lastWindowClosed()),
app, SLOT(quit()))
w = TreeView()
app.setMainWidget(w)
o=QObject()
p=Parser(o)
QObject.connect(o, PYSIGNAL("sigDocType"),
w.startDocument)
QObject.connect(o, PYSIGNAL("sigStartTag"),
135
Chapter 7. Signals and Slots in Depth
w.startElement)
QObject.connect(o, PYSIGNAL("sigEndTag"),
w.endElement)
s=open(args[1]).read()
p.start(s)
w.show()
app.exec_loop()
else:
print "Usage: python qtparser.py FILE.xml"
if __name__=="__main__":
main(sys.argv)
Here we create a QObject which is used to emit all necessary signals, since we
cannot inherit from more than one PyQt class at the same time. Note that by
using this technique, you dont have to subclass from QObject in order to be
able to emit signals. Sometimes delegation works just as well.
A parser object is created, with the QObject object as its argument.
Before feeding the parser the text, all connections we want are made from the
QObject object (which we passed to the parser to make sure it can emit signals)
to the TreeView object that forms the main window.
The file whose name was given on the command line is read and passed on to
the parser. I have included a very small test file, test.xml, but you can use any
Designer UI design file.
This is a very simple and convenient way of working with XML files and PyQt
guis but its generally useful, too. The standard way of working with XML files
and parsers allows for only one function to be called for each tag. Using signals and
slots, you can have as many slots connected to each signal as you want. For
instance, you can have not only a gui, but also an analyzer that produces statistics
listening in on the same parsing run.
136
Chapter 7. Signals and Slots in Depth
On a final note, there is one bug in this code... See if you can find it, or consult
Section 10.6.3 for an explanation.
7.5. Conclusion
We have seen the use of signals and slots in GUIs and in abstract data models.
Using signals and slots is appropriate if you are creating objects that should be kept
as separate from one another as possible, while still being able to communicate with
each other. Signals and slots are an efficient and maintainable way of creating
highly reusable software components.
137
Chapter 7. Signals and Slots in Depth
138
Chapter 8. String Objects in Python
and Qt
Most likely, you wont need the information in this chapter very often. If you dont
juggle character encodings on a regular basis, or work extensively with Unicode,
then you can probably get by quite well with the automatic string handling of PyQt.
However, situations may arise, especially when working with string-intensive
applications, where PyQts behavior might surprise you. Then you will probably
find yourself coming to this chapter.
8.1. Introduction
Working with strings is a delight in Python. Take the simple fact that you can,
depending on your whim and on whether the string contains the other kind, choose
to enclose your string literals in single (), double ("), or triple ( or """) quotes.
Triple quoted strings can span multiple lines no more string concatenations just
because the string doesnt fit the line.
Once you have your string, and it can be a delightfully long string, megabytes if
needs be, you can transform it, mangle it, search in it all using a few choice
modules, such as string, re or the native methods of the string object. About
the only snag is the immutability of strings every modifying action creates a new
string from the old, which can be costly.
In C++, working with strings is not a delight. Working with strings in C++ requires
using null-terminated character arrays, and writing all your own support functions.
Or you have to try to use the C++ Standard Library String class, which is rather
limited. This is why Trolltech created two string classes QString and
QCString which are almost as powerful and friendly to use as the Python string
classes. In fact, when Trolltech first created QString, there was no string class in
the standard C++ library.
Python also has two string classes: the old string class, in which every byte
represents a character, and the newer Unicode string class, which contains a
sequence of Unicode characters that can, depending on the encoding, take between
one and four bytes. The Qt QString class is equivalent to the Python Unicode
139
Chapter 8. String Objects in Python and Qt
string class, and the Qt QCString class is more like the old 8-bit Python
string.
Your friendly Python Library Reference will tell you all about the string module,
the string class, and the re module for regular expression matching. In this
chapter I am more concerned with the interaction between QString and Python
strings, and with character encoding questions.
#
# qstring1.py - saving a QString to a file
#
from qt import *
140
Chapter 8. String Objects in Python and Qt
# Construct a Qt QString
f=open("richard", "w+")
f.write(pyString)
f.flush()
f.write(qtString)
f.close()
There are good reasons for this behavior. Returning QStrings from widgets gives
the developer access to all the neat Qt string handling functionality. A Qt string is
mutable, in contrast to a Python string, and having the Qt QString makes it easier
to change the contents in place. Lastly, returning a Qt string instead of a Python
string avoids a somewhat costly conversion which might not be needed if all you
want to do is to stuff the text in another Qt widget.
Of course, the downside is that if you want to treat a QString object as a Python
string, youll have to convert it yourself, using one of the Python built-in functions
str() or unicode(). Adapting the previous script makes it work as expected:
#
# qstring2.py - saving a QString to a file
141
Chapter 8. String Objects in Python and Qt
from qt import *
# Construct a Qt QString
f=open("richard", "w+")
f.write(pyString)
f.flush()
f.write(str(qtString))
f.close()
I dont need to show you screen output here it just works. You will have to pay
attention to what happens with the strings you receive from Qt widgets. If you want
to write the contents to a file, database, or to mangle the string with Python
modules, you will need to explicitly convert the QString object to Python strings. If
you want to feed the string to another widget, you dont have to do anything.
142
Chapter 8. String Objects in Python and Qt
which contain characters in the Unicode encoding. Unicode is a complex topic that
is treated in the next section; this section deals with simple strings.
QCString is the PyQt equivalent of the Python simple string. The Qt
documentation describes QCString as a weak class, which is accurate. The
implementation does not feature all the intelligence and care that has gone into
QString, and as a consequence it scales poorly to large strings.
#
# empty.py - feeding zero bytes to a QCString
#
from qt import *
pystring=abc\0def
print "Python string:", pystring
print "Length:", len(pystring)
qcstring=QCString(pystring)
print "QCString:", qcstring
print "Length:", qcstring.length()
143
Chapter 8. String Objects in Python and Qt
Except for this proviso, both QCString and the Python string object are equivalent,
and you can use the Python string object wherever a QCString is needed as a
parameter in a function. You can convert the QCString back to a python string with
the str() function. If the QCString is empty, i.e., it contains only one byte with
the value zero (\0), an empty Python string is returned, not a Python string that
contains one zero byte.
The issue of null versus empty strings is an interesting one. A null QCString is
constructed as follows:
nullstring=QCString()
This string is conceptually equivalent to the Python None object, except that the
null QCString has a type. There is no way to construct a null Python string: a
Python string without contents is always empty, i.e. the equivalent of a QCString
that contains one byte with the value zero. The following script attempts a few
combinations, using Pythons built-in assert function.
Assert: The assert statement is one of the more useful tools in the Python
developers toolchest. You can use assert to check any statement for truth
and if it fails, an AssertionException is thrown. If you compile your Python
scripts to optimized bytecode (.pyo files), then the assertion statements are
removed, making assert ideal for checking your code for invalid entry
conditions in method calls during development. The use of assert in the
following script is more of a hack: this little script wouldnt do anything if run
with python -O null.py; only the line print message, "TRUE" would be executed
in the assertTrue function.
Example 8-4. null.py - empty and null QCStrings and Python strings
#
# null.py - empty and null QCStrings and Python strings
#
from qt import QCString
144
Chapter 8. String Objects in Python and Qt
assertTrue(emptypystring==emptyqcstring,
"Empty Python string equals empty QCString")
assertTrue(emptypystring==str(emptyqcstring),
"Empty Python string equals str(empty QCString)")
assertTrue(emptypystring==str(nullqcstring),
"Empty python string equals str(null QCString)")
assertTrue(nullpystring==emptyqcstring,
"Python string containing 0 byte equals empty QCString")
assertTrue(nullpystring==str(emptyqcstring),
"Python string contain-
ing 0 byte equals str(empty QCSTRING)")
assertTrue(nullqcstring is None,
"Null QCString equals None object")
145
Chapter 8. String Objects in Python and Qt
Of course, some of these concerns hold for QString, too. It is equally possible to
have an empty QString or a null QString. Note that embedding a zero byte in a
Python string and then feeding it to a QString shows the same behavior as with
QCString, even though QString isnt a null-terminated string class:
#
# emptyqstring.py - feeding zero bytes to a QString
#
from qt import *
pystring=abc\0def
print "Python string:", pystring
print "Length:", len(pystring)
qstring=QString(pystring)
print "QString:", qstring
print "Length:", qstring.length()
The unavoidable conclusion is that you shouldnt try to use Python strings as
containers for binary data and then convert them to Qt string objects. Of course,
theres a solution: you can use QByteArray to store binary data.
146
Chapter 8. String Objects in Python and Qt
147
Chapter 8. String Objects in Python and Qt
Devanagari letters is fairly small, but the set of ligatures runs into the hundreds.
Those ligatures are not defined in the character set, but have to be present in fonts.
Scripts like Arabic or Burmese are even more complicated. For those scripts,
special rendering engines have to be written in order to display a text correctly.
From version 3, Qt includes capable rendering engines for a number of scripts, such
as Arabic, and promises to include more. With Qt 3, you can also combine several
fonts to form a more complete set of characters, which means that you no longer
have use have one monster font with tens of thousands of glyphs.
The next problem is inputting those texts. Even with remappable keyboards, its still
a monster job to support all scripts. Japanese, for instance, needs a special-purpose
input mechanism with dictionary lookups that decide which combination of sounds
must be represented using Kanji (Chinese-derived characters) or one of the two
syllabic scripts, kana and katakana.
There are still more complications, that have to do with sort order, bidirectional text
(Hebrew going from right to left, Latin from left to right) then there are vested
problems with determining which language is the language of preference for the
user, which country he is in (I prefer to write in English, but have the dates show up
in the Dutch format, for instance). All these problems have their bearing upon
programming using Unicode, but are so complicated that a separate book should be
written to deal with them.
However, both Python strings and Qt strings support Unicode and both Python
and Qt strings support conversion from Unicode to legacy character sets such as the
wide-spread Latin-1, and vice-versa. As said above, Unicode is a multi-byte
encoding: that means that a single Unicode character is encoded using two bytes. Of
course, this doubles memory requirements compared to single-byte character sets
such as Latin-1. This can be circumvented by encoding Unicode using a variable
number of bytes, known as UTF-8. In this scheme, Unicode characters that are
equivalent to ASCII characters use just one byte, while other characters take up to
three bytes. UTF-8 is a wide-spread standard, and both Qt and Python support it.
Ill first describe the pitfalls of working with Unicode from Python, and then bring
in the Qt complications.
148
Chapter 8. String Objects in Python and Qt
Of course, it would be nice if we could at least type the strings directly in UTF-8, as
shown in the next screenshot:
149
Chapter 8. String Objects in Python and Qt
Unfortunately, this wont work either. Hidden deep in the bowels of the Python
startup process, a default encoding is set for all strings. This encoding is used to
convert from Unicode whenever the Unicode string has to be presented to outside
world components that dont talk Unicode, such as print. By default this is 7-bits
ASCII. Running the script gives the following error:
The default ASCII encoding that Python assumes when creating Unicode strings
means that you cannot create Unicode strings directly, without explicitly telling
Python what is happening. This is because Python tries to convert from ASCII to
utf8, and every byte with a value greater than the maximum ASCII knows (127)
will lead to the above error. The solution is to use an explicit encoding.The
following script will work better:
If you run this script in a Unicode-enabled terminal, like a modern xterm, you will
see the first line of the Nala neatly printed. Quite an achievement!
150
Chapter 8. String Objects in Python and Qt
You can find out which encodings your version of Python supports by looking in the
encodings folder of your Python installation. It will certainly include mainstays
such as: ascii, iso8859-1 to iso8859-15, utf-8, latin-1 and a host of MacIntosh
encodings as well as MS-DOS codepage encodings. Simply substitute a dash for
every underscore in the filename to arrive at the string you can use in the encode()
and decode() functions.
#
# readutf8.py - read an utf-
8 file into a Python Unicode string
#
151
Chapter 8. String Objects in Python and Qt
def usage():
print """
Usage:
def main(args):
if len(args) < 1:
usage()
return
files=[]
print "Reading",
for arg in args:
print arg,
f=open(arg,)
s=f.read()
u=unicode(s, utf-8)
files.append(u)
print
files2=[]
print "Reading directly as Unicode",
for arg in args:
print arg,
f=codecs.open(arg, "rb", "utf-8")
u=f.read()
files2.append(u)
print
for i in range(len(files)):
if files[i]==files2[i]:
print "OK"
if __name__=="__main__":
main(sys.argv[1:])
As you can see, you either load the text in a string and convert it to a Unicode
string, or use the special open function defined in the codecs module. The latter
152
Chapter 8. String Objects in Python and Qt
option allows you to specify the encoding when opening the file, instead of only
when writing to the file.
#
# unichar.py Building strings from single chars.
#
import string, codecs
CYRILLIC_BASE=0x0400
uList=[]
for c in range(255):
uList.append(unichr(CYRILLIC_BASE + c))
f=open("cyrillic2.ut8", "aw+")
f.write(u.encode("utf-8"))
153
Chapter 8. String Objects in Python and Qt
f.flush()
Note that even if you construct your Unicode string from separate Unicode
characters, you will still need to provide an encoding when printing (utf-8, to be
exact). Note also that when writing text to a file, you will need to explicitly tell
Python that you are not using ASCII.
Another way of adding the occasional Unicode character to a string is by using the
\uXXXX escape codes. Here XXXX is a hexadecimal number between 0x0000 and
0xFFFF:
About codecs and locales: With all this messing about with codecs you will
no doubt have wondered why Python cant figure out that you live in, say,
Germany, and want the iso-8950-1 codec by default, just like the rest of your
system (such as your mail client, your wordprocessor and your file system)
uses. The answer is twofold. Python does have the ability to determine from
your system which codec it should use by default. This feature, however, is
disabled, because it is not one-hundred percent reliable. You can enable that
code, or change the default codec system-wide, for all Python programs you
use, by hacking the site.py file in your Python library directory:
if 0:
# Enable to support locale aware default string encodings.
import locale
loc = locale.getdefaultlocale()
if loc[1]:
154
Chapter 8. String Objects in Python and Qt
encoding = loc[1]
...
if encoding != "ascii":
sys.setdefaultencoding(encoding)
Either change the line encoding = "ascii" to the codec associated with the
locale you live in, or enable the locale aware default string encodings by
setting the line if 0: to if 1:.
It would be nice if you could call sys.setdefaultencoding(encoding) to set
a default encoding for your application, such as utf-8. But, and you dont want
to hear this, this useful function is intentionally deleted from the sys module
when Python is started, just after the file site.py is run on startup.
What can one do? Of course, its very well to assume that all users on a
system work with one encoding and never make trips to other encodings; or to
assume that developers dont need to set a default encoding per application,
because the system will take care of that, but Id still like the power.
Fortunately, theres a solution. Ill probably get drummed out of the regiment
for suggesting it, but its so useful, Ill tell it anyway. Create a file called
sitecustomize.py as follows:
#
# sitecustomize.py - saving a useful function. Copy to the
# somewhere on the Python path, like the site-
packages directory
#
import sys
sys.setappdefaultencoding=sys.setdefaultencoding
Make this file a part of your application distribution and have it somewhere on
the Python path which is used for your application. This file is run
automatically before site.py and saves the useful function
setdefaultencoding under another name. Since functions are simply
155
Chapter 8. String Objects in Python and Qt
references to objects and those objects are only deleted when the last
reference is deleted, the function is saved for use in your applications.
Now you can set UTF-8 as the default encoding for your application by calling
the function as soon as possible in the initialization part of your application:
#
# uniqstring3.py -
coercing Python strings into and from QStrings
#
from qt import QString
import sys
sys.setappdefaultencoding("utf-8")
print s
print u
156
Chapter 8. String Objects in Python and Qt
Example 8-10. uniqstring1.py - coercing Python strings into and from QStrings
#
# uniqstring1.py -
coercing Python strings into and from QStrings
#
from qt import QString
qs=QString(s)
qu=QString(u)
print str(qs)
print str(qu)
If theres a chance that there are non-ASCII characters in the QString you want to
convert to Python, you should create a Python unicode object, instead of a string
object, by applying unicode to the QString.
157
Chapter 8. String Objects in Python and Qt
Example 8-11. uniqstring2.py - coercing Python strings into and from QStrings
#
# uniqstring2.py -
coercing Python strings into and from QStrings
#
from qt import QString
qs=QString(s)
qu=QString(u)
print unicode(qs)
print unicode(qu)
158
Chapter 9. Python Objects and Qt
Objects
This chapter delves into the construction of Python and C++ objects. This is a
complex topic, and not really required if you are only interested in getting started
with your project. However, when you feel that your objects are disappearing from
under your hands, or if youre leaking memory like a sieve, then this is the place to
turn to.
159
Chapter 9. Python Objects and Qt Objects
of explicitly deleting objects. Python manages all objects for you. It does this by
keeping track of references to every object. A reference is a variable, or an entry in
a list that represents an object. For instance, run:
#
# refs.py
#
class theClass: pass
anObject=theClass()
aList=[anObject]
aDictionary={"key": anObject}
print anObject
print aList
print aDictionary
This will result in one object with three references, as you can see from the result of
the print statements:
The object instance (0x81dcb4 is the objects id hash) will only be deleted when the
last reference is deleted. It is possible for references to disappear by going out of
scope. If the references are created inside a function, then as soon as the function is
finished running, the references disappear. References to variables can also be
attached to both classes (a class is an object in Python), and to objects. In the first
case, if the class disappears, then the references disappear. In the second case, if the
last reference to the object disappears, all references that object has to other
objects disappear, too.
160
Chapter 9. Python Objects and Qt Objects
#
# circular.py - circululululular references in Python
#
class B: pass
class A:
def __init__(self):
self.b=B()
self.b.a=self
a=A()
print a
print a.b
print a.b.a
print a.b.a.b.a.b.a.b.a.b.a.b.a.b.a.b.a.b.a.b.a.b.a
If you delete the instance a, you only make the objects inaccessible; because b still
refers to a, theres a reference for the reference counter, and a will not be destroyed.
161
Chapter 9. Python Objects and Qt Objects
Thus b will not be destroyed either, which means the reference to a remains in
existence ad infinitum! (Or at least until the Python interpreter shuts down.)
162
Chapter 9. Python Objects and Qt Objects
Mostly you wont need to concern yourself with this problem, since PyQt knows
exactly when to transfer ownership of C++ instances automatically.. Complications
arise if you create QObject derived objects that own, through the QObject
parent-child mechanism, other objects. (This ownership of objects by other objects
is one of the places where Qt deviates from the C++ standard practice, where the
object that creates another object should also take care of deleting it.)
#
# qtrefs1.py
#
import sys
from qt import *
class MainWindow(QMainWindow):
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
163
Chapter 9. Python Objects and Qt Objects
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
#
# qtrefs2.py
#
import sys
from qt import *
class MainWindow(QMainWindow):
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
164
Chapter 9. Python Objects and Qt Objects
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Does this mean that you always need to keep a reference to all Qt objects yourself?
This would make creating complex applications quite a drag! Fortunately, sip is
more clever than it seems. QObject derived objects stand in a owner-ownee (or
parent-child) relation to each other. Sip knows this, and creates references to child
objects on the fly, and decreases those references if the parents are deleted. (The Qt
library does something similar if you program in C++. This gives a kind of Java-like
flavor to C++ which is not appreciated by everyone).
To keep a widgets child alive, enter the parent object in the parent argument of
the child constructor, in this case, this is the second argument to the QPushButton
constructor:
#
# qtrefs3.py
#
import sys
from qt import *
class MainWindow(QMainWindow):
165
Chapter 9. Python Objects and Qt Objects
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app,
SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Note however these two important side-effects: The first is that this button, now that
it is owned by the main window, appears inside the main window. The second is
that you no longer need to explicitly call the function show() on the button.
As another side-effect of explicitly parenting objects, you need to be aware of who
owns an object before you can be sure that it will be deleted: your Python
application or another Qt object.
The trick is to determine who exactly owns the widget in question. Everything that
is derived from QObject has the function parent(), which can be used to
determine the owner of a widget. You can use the function removeChild to
remove the widget itself. Using parent() is often easier than remembering who
exactly owned the widget you want to get rid of.
self.parent().removeChild(self)
If you execute this incantation, the poor widget will be orphaned, and a Python del
statement on the Python reference will definitively remove the child.
#
# qtrefs4.py - removing a widget
#
import sys
from qt import *
166
Chapter 9. Python Objects and Qt Objects
class MainWindow(QMainWindow):
def removeButton(self):
self.removeChild(self.parentedButton)
del self.parentedButton
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Pressing the button will remove it, first by removing the ownership relation between
win and self.parentedButton and then removing the Python reference to the
object.
It is possible to retrieve the children of a certain QObject object by calling
children on QObject. Sip is clever enough to return the Python wrapper object
associated with that instance (rather than the actual C++ object instance).
167
Chapter 9. Python Objects and Qt Objects
#
# children.py
#
import sys
from qt import *
class MainWindow(QMainWindow):
self.setCentralWidget(mainwidget)
printChildren(self, " ")
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
168
Chapter 9. Python Objects and Qt Objects
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
What you cannot see here is the parallel structure of QLayoutItems that proxy for
the widgets. For that you need to use the QLayoutIterator that is provided by the
iterator() method of QListViewItem. Here, next(), both returns the next
item, and moves the iterator onwards.
#
# children.py
#
import sys
from qt import *
169
Chapter 9. Python Objects and Qt Objects
class MainWindow(QMainWindow):
self.setCentralWidget(mainwidget)
printChildren(layout, " ")
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
170
Chapter 9. Python Objects and Qt Objects
Finally, lets test the ownership rules of Qt and Python objects using the interactive
Python interpreter. In the following example, we create an object self.o, owned by
PyQt, and then a child object is created, not owned by the instance of class A, but as
a Qt child of object self.o. Thus, PyQt owns a and self.o, and Qt owns child,
and child doesnt get deleted, even when the Python reference goes out of scope.
On the other hand, the following wont work, because as soon as the execution flow
leaves the constructor, o is garbage collected, and child, is then garbage-collected,
too, since it isnt owned by a Qt object, and Python doesnt have a reference to it
anymore, either.
>>> class B:
... def ___init__(self):
... o=QObject()
... child = QObject(o)
...
>>> b=B()
>>> b.o
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: o
171
Chapter 9. Python Objects and Qt Objects
On the other hand, it isnt necessary to keep a Python reference to all created
objects: as long as the ultimate parent object is owned by PyQt, everything will go
well:
>>> class C:
... def __init__(self):
... self.o = QObject()
... self.child = QObject(self.o)
...
>>> c = C()
>>> c.o
<qt.QObject instance at 0x8263f54>
>>> c.o.children()
[<qt.QObject instance at 0x821d334>]
>>> c.child
<qt.QObject instance at 0x821d334>
>>>
As you see, it isnt necessary to keep a reference to child,- because PyQt is the
owner of the first object (because it has no Qt parent but a reference to a Python
object) but Qt is the owner of the second widget (because it does have a parent) and
so the C++ instance (qt.QObject instance at 0x821d334) is not deleted when the
corresponding Python object goes out of scope.
What if your Python class were a subclass of QObject?:
172
Chapter 9. Python Objects and Qt Objects
As you can see, o doesnt get deleted, nor child - both are owned by Qt and will
be deleted as soon as object d is deleted. You can still reach these objects by using
the children() function QObject provides.
This layer between Python and Qt is implemented in the sip library sip not only
generates the wrapper code, but is a library in its own right, containing functionality
for the passing of object references between C++ and Python.
Sip is also responsible for the reference counting mechanisms. In most cases, Sip is
clever enough to closely simulate Python behavior for C++ Qt objects. As you saw
in the previous example, contrary to what happens in C++, when you remove the
last reference to a C++ object, it will be automatically deleted by Sip.
173
Chapter 9. Python Objects and Qt Objects
#
# sigslot.py - a simple signals/slots implementation in Python
#
class ClassA:
def __init__(self):
self.interestedObjects=[]
def sendSignal(self):
for obj in self.interestedObjects:
obj.slot("This is a signal from ClassA")
class ClassB:
objectA=ClassA()
objectB=ClassB()
objectA.connect(objectB)
objectC=ClassB()
objectA.connect(objectC)
objectD=ClassB()
objectA.connect(objectD)
objectA.sendSignal()
174
Chapter 9. Python Objects and Qt Objects
the programmer, who would have to remember to sever all connections by hand. We
all know what happens when a programmer has to remember cleaning up after
him...
Fortunately, the implementation of signals and slots in sip is not quite so basic. Sip
works together with the signals and slots implementation in Qt, which is highly
complex, and involves fooling around with a special-purpose macro processor. This,
at least, Python developers are spared.
Sip keeps special proxy objects around to handle the signal/slot connections. If you
use a recent version of Python (>2.1), the actual connections will not need real
references, but can work with the new-fangled weak reference concept. Weak
references are references that dont count for the purpose of reference counting.
This is good, because your application will not crash if a signal is emitted that was
connected to a slot in a deleted object and created connections will not keep
objects alive.
Chapter 7 deals with signals and slots in far more depth.
175
Chapter 9. Python Objects and Qt Objects
>>> a=A()
>>> a.className() A
>>> a.inherits(QTimer) 1
For interesting Python introspection functions you should consult the Python
language reference but the equivalent using Python idioms of the above session
would be:
Object introspection is especially useful if you dabble in the black art known as
meta-programming that is, creating a program that run-time constructs some of
the classes it needs. Heaps of fun but not always innocent fun.
176
Chapter 10. Qt Class Hierarchy
In this chapter I will present an overview of the Qt library, including both gui
objects and non-gui objects. While well-designed, Qt is a large library, and the key
to effective use is not knowing every class by heart, but rather developing an
intuition for what is available and where it is. After an overview of the entire
hierarchy I will shortly discuss the base classes, the gui classes and compare the Qt
utility classes with their Python equivalents.
10.1. Hierarchy
As noted before, Qt consists of a hierarchy of classes derived from a basic QObject
class, and a side-show cluster of more independent classes. Classes derived from
QObject share some important functionality, namely the power to communicate
through signals and slots and to arrange themselves in an ownership hierarchy.
There are other odds and ends, such as introspection functionality, which is
discussed in Section 9.7.
177
Chapter 10. Qt Class Hierarchy
QObject QAction
QApplication
QCanvas
QClipboard
QGridLayout
QPushButton
QRadioButton
QToolButton
QComboBox
QMessageBox
QTabDialog
QWizard
QFrame QGrid
QLabel
QMenuBar
QScrollView QCanvasView
QIconView
QListBox
QListView
QTable
QTextView
QLineEdit
QMainWindow
QStatusBar
QPainter
178 QPixMap
Chapter 10. Qt Class Hierarchy
Prior to version 3.0, PyQt basically plunked everything except for the OpenGL
extension in the qt module. That was the situation when I wrote this book. From
PyQt 3.0, the Qt modules Canvas, IconView, Network, OpenGL, SQL, Table,
WorkSpace and XML have been put in separate Python modules.
179
Chapter 10. Qt Class Hierarchy
Timers
Object ownership hierarchy
Event handling and event filters
Introspection
Properties
Signals and slots are meant for communication between objectsfor instance,
when a button is pressed, certain other objects must be notified. Events, on the other
hand, notify objects of general actions of the user, such as key presses or mouse
movements; events do not necessarily originate with objects.
The linchpin of the event handling mechanism is the class representing the
information associated with an event, such as the position of the mouse. This is the
QEvent class; there are a whole slew of specialized subclasses like QPaintEvent,
QFocusEvent, QMouseEvent, QWheelEvent and QKeyEvent that do the rounds of
all interested objects. For instance, a keypress is first passed to the application
object, based on QApplication. From there it trickles down the whole widget
ownership hierarchy until one widget consumes it that is, reacts to the event and
doesnt send it on by calling the event(QEvent) method.
See the next listing for an example of reacting to mouse presses and movements.
Note also that if the window is obscured and remapped, the paintEvent method is
fired this will obliterate your whole beautiful drawing.
#
# event1.py
#
from qt import *
import sys
class Painting(QWidget):
180
Chapter 10. Qt Class Hierarchy
self.p.begin(self)
self.p.fillRect(self.rect(), QBrush(Qt.white))
self.p.flush()
self.p.end()
self.p.flush()
self.p.end()
class MainWindow(QMainWindow):
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
181
Chapter 10. Qt Class Hierarchy
event1.py
182
Chapter 10. Qt Class Hierarchy
could be the command to log in. Associated with this command is a short help text
that appears as a tooltip, a longer help text that might appear in the status bar, an
icon that is used in the toolbar, a short text for use in the menu, and an accelerator
key that is used from the keyboard. The log in action can be enabled or disabled
(when the network is down, for instance). You do not want to distribute all this
functionality all over your application.
A QAction ties everything related to an action together, and can be added to
toolbars and menus. When performed, a QAction emits an activated() signal.
The following is a simple example with an action, a menubar, a toolbar and a
statusbar:
Example 10-2. action.py - Using a QAction to group data associated with user
commands
#
# action.py
#
import sys
from qt import *
connectIcon=["16 14 5 1",
" c None",
". c black",
"X c gray50",
"o c red",
"O c yellow",
" ",
" . ",
" X .X ",
" XooX . ",
" Xoooo .X ",
" XooooooX ",
" XooooooX ",
" XoooooX. ",
" XooooX. ",
" XOXXXX. ",
" XOXX... ",
" XOXX ",
183
Chapter 10. Qt Class Hierarchy
" XX ",
" X "
]
class MainWindow(QMainWindow):
# Define action
self.action=QAction(self, "login")
self.action.setText("Log in")
self.action.setMenuText("&Login")
self.action.setToolTip("Login to the central server")
self.action.setWhatsThis("Logs in to the cen-
tral server.")
self.action.setStatusTip("Log in to the cen-
tral server.")
self.action.setAccel(Qt.CTRL + Qt.Key_L)
self.action.setIconSet(QIconSet(QPixmap(connectIcon)))
self.connect(self.action,
SIGNAL("activated()"),
self.slotAction)
# Statusbar
self.statusBar=QStatusBar(self)
# Define menu
self.menu=QPopupMenu()
self.action.addTo(self.menu)
self.menuBar().insertItem("&File", self.menu)
# Define toolbar
self.toolBar=QToolBar(self, Main)
self.action.addTo(self.toolBar)
184
Chapter 10. Qt Class Hierarchy
def slotAction(self):
QMessageBox.information(self,
"Network Client",
"Connecting to server...")
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
action.py
185
Chapter 10. Qt Class Hierarchy
that dont show up in their taskbar. In fact, a large percentage of users have trouble
when there is more than one window on their desktop.
However, the functionality is available, and it might be useful for your application.
Lets take our high-powered graphics editor, from the event1.py example, and give
the user ten windows to scribble in. All that is needed is it to add the Painting to a
QWorkspace object, instead of setting it as the central widget of the MainWindow.
Realistically, youll want to offer menu options for selecting, tiling and cascading
the windows. QWorkSpace provides a tile() and a cascade() slot for these
purposes, as well as a windowList that returns a list of all windows. While it is a
bad idea to limit your users to a small maximum number of documents, if you let
them open more, you should provide a separate window with a full list. Having
more than ten windows to select from in a menu makes working difficult.
...
class MainWindow(QMainWindow):
186
Chapter 10. Qt Class Hierarchy
QWidget can be useful to build your own widgets on, provided you are prepared to
do all your own painting this includes buffering in case your widget gets a
paintEvent call! Consider the next snippet, which is an extension of the event1.py
example:
#
# event2.py
#
from qt import *
import sys
class Painting(QWidget):
187
Chapter 10. Qt Class Hierarchy
class MainWindow(QMainWindow):
def main(args):
188
Chapter 10. Qt Class Hierarchy
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
10.4.1. QColor
The QColor class represents any color that can be used in PyQt. You can instantiate
a new color either by using an RGB (red-green-blue) value, an HSV
(hue-saturation-value) value, or a name. The X11 system used on Unix provides a
database full of rather poetic color names like Old Lace, Royal Blue and Peach
Puff you can use these names instead of hexadecimal numbers. The Windows
version of PyQt has a copy of this database, so its quite portable. If you replace the
189
Chapter 10. Qt Class Hierarchy
resizeEvent() in the event2.py example with the following code, youll see the
effect:
...
def resizeEvent(self, ev):
tmp = QPixmap(self.buffer.size())
bitBlt(tmp, 0, 0, self.buffer)
self.buffer.resize(ev.size())
self.buffer.fill(QColor("peachpuff"))
bitBlt(self.buffer, 0, 0, tmp)
...
event3.py
A final note on colors: the way you set the colors of a widget have been changed
between Qt2 and Qt3. Where you first used setBackgroundColor() , youd now
use setEraseColor() . Yes, there is a logic behind this change of name, but it is
very specious, and the change broke almost all my code. The erase color is the color
that Qt uses to clear away, or erase, all the pixels that had been painted just before
they are painted again in a paint event.
When youre designing complex widgets, you will want to investigate
setBackgroundMode and the BackgroundMode flags.
190
Chapter 10. Qt Class Hierarchy
QImage. Where QPixmap and QBitmap are optimized for drawing (and then
showing on screen or on a printer), QImage is optimized for reading and writing
(together with the QImageIO class), and for manipulating the actual pixels of an
image. Theres another image-related class, QPicture, which can be used to record
drawing operations and replay them later. The recorded paint events can then be
stored in a file and reloaded later on. Those files are called meta-files but theyre
in a special Qt format. In Qt 3, QPicture also supports the standard scalable vector
graphics format, svg. If you want to create a complex vector-drawing application
youd be well advised to stick to this standard.
10.4.3. QPainter
A QPainter object is used to efficiently paint on any paintdevice using a variety of
primitive graphics, such as simple dots or lines, bezier curves, polygons, strings of
text (using a particular font) or pixmaps. Drawings can be modified, for instance by
shearing or rotating, and parts can be erased or clipped. We have already used a
QPainter to draw the scribbly lines in previous examples.
Paint devices can be:
pictures: QPicture
pixmaps: QPixmap
printers: QPrinter
191
Chapter 10. Qt Class Hierarchy
#
# action2.py
#
import sys
from qt import *
connectIcon=QPixmap(["16 14 5 1",
" c None",
". c black",
"X c gray50",
"o c red",
"O c yellow",
" ",
" . ",
" X .X ",
" XooX . ",
" Xoooo .X ",
" XooooooX ",
" XooooooX ",
" XoooooX. ",
" XooooX. ",
" XOXXXX. ",
" XOXX... ",
" XOXX ",
" XX ",
" X "
])
class MainWindow(QMainWindow):
192
Chapter 10. Qt Class Hierarchy
Chapter 21 deals with painters and paintdevices in quite a lot of detail, while
Chapter 24 deals with printing to paper.
10.4.4. QFont
There is no other area where there are so many and profound differences between
operating systems as there is with fonts. And if you take into account the difference
in font handling between printers and screens, you will get a feeling for how
difficult it is to get proper and dependable cross-platform multi-lingual font support
in a toolkit.
Fortunately, Qts font support has steadily improved, and is now at the point where,
provided good quality fonts are available on a system, it can offer the same
excellent screen and printer support on all platforms.
The first issue is the font used for drawing labels and other application texts
sometimes called the system font. This naturally differs for each system: Windows
uses Arial these days, while KDE uses Helvetica, CDE Times and OS X a bold
Helvetica. Furthermore, the system font is also often customized by the user. Text in
one font takes more room than text in another font possibly giving ugly display
errors. By using Qts layout managers, instead of positioning widgets with
pixel-precision yourself, you will have little trouble dealing with the geometry
differences between Windows Arial font and KDEs Helvetica standard all
controls will reflow neatly.
For handling fonts in your application you can work with QFont. Qt builds its own
database of available fonts from whatever the system provides. You can then access
these fonts in a system-independent manner, without having to juggle X11 font
resource names yourself.
193
Chapter 10. Qt Class Hierarchy
QFont provides all necessary functions to select encodings (or scripts in Qt3), font
families, styles and sizes. Theres also a standard dialog available, QFontDialog
that you can use to let the user select a certain font.
There are serious differences between the font system in Qt2 and Qt3. In Qt2, you
need to determine which character set encoding you need; and you can only use the
character set encodings that the particular font supports. For instance, if your font
supports the KOI8 Cyrillic encoding, then that is the encoding you can use. The font
you request has a one-to-one relation with the font files on your system.
In Qt3, you select fonts by name, style and script (like Cyrillic), and Qt will select
the closest fitting font. If your widget needs to present text on screen that uses
characters that cannot be retrieved from the selected font, Qt will query all other
fonts on your system, and assemble a composite, synthetic font that includes all
characters you need. You lose some control but you gain a correct representation of
all possible texts you can use any font for any text in any script.
If you want to set a certain font for the entire application, you can use the
QApplication.setFont class function. Likewise, everything that descends from
QWidget also has a setFont() function.
You can use QFontInfo to determine the exact font Qt uses for a certain QFont
but this might be quite slow. An important use of QFontInfo with Qt3 is to
determine whether the font you get was exactly the font you asked for. For instance,
if you desire a Bembo font, which might not be present on your system, you could
get something closeish: a Times New Roman. Especially for drawing and dtp
applications its important to be sure which font is actually used.
QFontMetrics can be used to determine metrics information about a font. For
instance, how high the ascenders and descenders are, and how wide the widest
character is. This is useful if you need to determine how much space a line of text
takes when printed on paper.
Ascender height
194
Chapter 10. Qt Class Hierarchy
Font metrics
195
Chapter 10. Qt Class Hierarchy
196
Chapter 10. Qt Class Hierarchy
10.5.1. QFrame
Frames are used to group other widgets either visibly (for instance by drawing a
nice bevel around them), or invisibly (by managing the geometry of those widgets.
PyQt offers all the usual options, from panels to ridges to bevels, with horizontal
and vertical lines thrown in for good measure.
197
Chapter 10. Qt Class Hierarchy
10.5.2. QPushButton
Pushbuttons are the mainstay of gui programming. They can be adorned with text or
with a picture, but not both (you need a QToolButton for that). QPushButtons are
based on an abstraction of all button functionality, namely QButton, which is also
the parent of QCheckBox, QRadioButton and QToolButton. In honor of
QPushButtons central importance, I want to present a Hello application with four
buttons, each in a different style. This also shows a frame.
#
# buttons.py
#
from qt import *
import sys
class MainWindow(QMainWindow):
self.grid=QGrid(2, self)
self.grid.setFrameShape(QFrame.StyledPanel)
self.bn4=QPushButton("Hello", self.grid)
self.setCentralWidget(self.grid)
198
Chapter 10. Qt Class Hierarchy
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
buttons.py
10.5.3. QLabel
Labels are ubiquitous in a gui application and the PyQt QLabel offers much
more than just plain-text labels for use in dialog boxes. PyQt labels can also contain
rich text or a QMovie, such as an animated GIF or PNG. Through the setBuddy
method, a QLabel can be associated with another control. If any character in the
label text is prefixed by an ampersand & that character will be shown
underlined, and by pressing alt-character, the user can jump to the control
associated with the label through the buddy property.
#
# label.py
199
Chapter 10. Qt Class Hierarchy
#
import sys
from qt import *
class dlgLabel(QDialog):
self.layout=QHBoxLayout(self)
self.layout.setSpacing(6)
self.layout.setMargin(11)
self.layout.addWidget(self.label)
self.layout.addWidget(self.edit)
if __name__ == __main__:
app = QApplication(sys.argv)
QObject.connect(app, SIGNAL(lastWindowClosed())
, app
, SLOT(quit())
)
win = dlgLabel()
app.setMainWidget(win)
win.show()
app.exec_loop()
label.py
200
Chapter 10. Qt Class Hierarchy
If you press alt-e after starting the label.py script (which is on the CD-ROM),
youll see the cursor appearing in the edit field.
You might wonder why the cursor is not the control that accepts user input when
you start the script this is a property of PyQt. On starting an application, the
main window has the focus, not the controls associated with it. If you want to make
the users life easier, call setFocus() on the main widget in the __init__ of the
main window:
#
# label2.py
#
import sys
from qt import *
class dlgLabel(QDialog):
self.layout=QHBoxLayout(self)
self.layout.setSpacing(6)
self.layout.setMargin(11)
self.layout.addWidget(self.label)
self.layout.addWidget(self.edit)
self.edit.setFocus()
if __name__ == __main__:
app = QApplication(sys.argv)
QObject.connect(app, SIGNAL(lastWindowClosed()),
app, SLOT(quit()))
win = dlgLabel()
201
Chapter 10. Qt Class Hierarchy
app.setMainWidget(win)
win.show()
app.exec_loop()
A label is a QWidget; it is thus quite possible to handle key and mouse events in a
label. You might, for instance, want to make a clickable label that looks like an
URL.
10.5.4. QRadioButton
Radio buttons always remind of my tax return forms - check one and only one out
of a certain number of choices. Radio buttons should not be used if there are more
than five choices at most, and you would be well advised to limit yourself to no
more than three. A constellation of radio buttons has the advantage that all options
are visible at the same time, but it takes a lot of screen space. Do not use
checkboxes instead of radio buttons for exclusive choices. People will get confused.
Radio buttons include their labels and these labels can again be marked with an
ampersand (&) for easy selection. In order to force the one and only one choice,
combine the mutually exclusive radio buttons in a QButtonGroup, or one of the
descendants: QVButtonGroup for vertical layouts (recommended) or
QHButtonGroup for horizontal layouts (these look rather weird). A radiobutton can
be initialized with setChecked().
#
# label.py
#
import sys
from qt import *
class dlgRadio(QDialog):
202
Chapter 10. Qt Class Hierarchy
if name == None:
self.setName("dlgRadio")
self.layout=QVBoxLayout(self)
self.radio1.setChecked(1)
self.layout.addWidget(self.buttonGroup)
if __name__ == __main__:
app = QApplication(sys.argv)
QObject.connect(app, SIGNAL(lastWindowClosed()),
app, SLOT(quit()))
win = dlgRadio()
app.setMainWidget(win)
win.show()
app.exec_loop()
radio.py
10.5.5. QCheckBox
Checkboxes are another of those gui features that makes one think of bureaucratic
forms. Check any that apply... Checkboxes are as easy to use as radiobuttons, and
come with a label attached, like radiobuttons. A variation which makes for instant
user confusion is the tri-state checkbox, in which theres a checked, unchecked and
a doesnt apply state the doesnt apply state is usually rendered to look just like a
203
Chapter 10. Qt Class Hierarchy
Now the user has three choices: either look for solvent persons, for insolvent
persons or for both. The Platinum style makes it very clear which state the
checkbox is in, compared to, for instance, the Windows style.
10.5.6. QListBox
Listboxes are simple containers where a variable number of strings can be added.
You can allow the user to select no item, one item, a range of items or a
discontinuous set of items. You can enter texts or pixmaps in a listbox, but the
listbox, like the listview and the combobox, doesnt let you associate arbitrary data
with the items inside it.
This is something you may often want you let the user select a certain object by
clicking on an item in the listbox, and you want that object to be available in your
application, not the string or picture that represents the object, or the index of the
item in the listbox. You can achieve this by coding a small associative listbox:
#
# listbox.py
#
# listbox with key-to-index and index-to-key mapping
#
import sys
from qt import *
204
Chapter 10. Qt Class Hierarchy
class AssociativeListBox(QListBox):
def currentKey(self):
return self.text2key[self.currentItem()]
class MainWindow(QMainWindow):
205
Chapter 10. Qt Class Hierarchy
self.connect(self.listbox,PYSIGNAL( "itemSe-
lected"), self.printSelection)
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
listbox.py
Of course, the same trick is needed to get something useful out of a QComboBox or
a QListView.
10.5.7. QComboBox
A QComboBox offers almost the same functionality as a QListBox, but folds out
and in, preserving screen space. The contents of a combobox can be read-only or
editable, and you can check the correctness of user input using a QValdidator
object.
206
Chapter 10. Qt Class Hierarchy
10.5.8. QLineEdit
This is a simple one-line edit control, familiar to gui users everywhere. It supports
copy, cut and paste and redo, too. Theres a special mode for password boxes.
10.5.9. QMultiLineEdit
QMultiLineEdit provides a very simple multi-line editor. This class does not, in
any way, support rich text all text is in the same font, same size and same color.
You can enable word-wrap, but thats about it. There are no limits on the amount of
text it can handle (as was the case with the old Windows edit control, which had a
limit of about 32 kb), but with megabytes of data, it will become decidedly slow.
With Qt3 this class has become obsolete. Youre supposed to use the new, advanced
QTextEdit, instead. After creating a QTextEdit object, youd set the textformat
to plain with QTextEdit.setFormat(Qt.PlainText) ; no user would notice the
difference. QTextEdit and QMultiLineEdit are quite different to the
programmer, though, and you can still use QMultiLineEdit if you need
compatibility with older versions of Qt.
10.5.10. QPopupMenu
One of the most useful things you can offer a user in any document-based
application is a context menu press the mouse-button anywhere in the document
and a list of useful options pop up. PyQts QPopupMenu can be used both as a
stand-alone popup menu, and within a menu bar. Menu items can have a shortcut
key associated with them, an accelerator and a small icon. The most useful way of
adding items to a menu is by defining a QAction. You can nest menus, and make
tear-off menus, where the user can click on a tear-off handle which puts the
menu in a window of its own.
10.5.11. QProgressBar
QProgressBar gives you a horizontal progressbar its quite simple, even
though it can be set to use one of several different styles. Theres also
207
Chapter 10. Qt Class Hierarchy
QProgressDialog which can be used for lengthy actions that completely block
access to the application. Since PyQt doesnt really support multi-threading, its
probably best to stick with the blocking dialog.
If you want to get fancy, you can, with a bit of juggling, get an approximation to
threading by using a QTimer. Then its best to place the progress bar in the
statusbar of your application, instead of a separate non-modal progress dialog.
Section 21.1.1 gives an example of the use of a timer.
208
Chapter 10. Qt Class Hierarchy
QToolButton is a special button that carries more often a picture than a text its
mostly used in toolbars, where you can add buttons without explicitly creating
instances of this class.
10.6.2. QTextEdit
Available only in Qt3, not in Qt2, QTextEdit is a rich text editing widget. This is a
very powerful class, almost a complete wordprocessor in its own right - except that
it doesnt have a notion of the concept of page. The KDE Office wordprocessor,
KWord is built around it.
209
Chapter 10. Qt Class Hierarchy
QTextEdit can display images, text in fancy fonts across the whole Unicode range,
tables and lists. Internally, QTextEdit uses the same subset of HTML that
QTextView and friends use. If your text is saved in a different file format, you will
first have to convert it to HTML, and that makes QTextEdit difficult to use for
purposes such as a programmers editor, but has everything needed to create a rich
text input pane for an email client, for instance. (Not that I condone sending
html-formatted email!)
#
# tree.py - a simple tree with QListView
#
import sys
from qt import *
class MainWindow(QMainWindow):
210
Chapter 10. Qt Class Hierarchy
self.items.append(QListViewItem(self.items[-
2], "child 2"))
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Note that inserting items in an unsorted QListView inserts the items at the top of
the listview. Thus, if you insert items A, B and C, in that order, the order in the
listview will be C, B, A. Try adding the following line in the constructor, before the
listviewitems are created:
self.tree.setSorting(1,-1)
If you want your latest item to be the last in the branch, then you will have to give
the item it comes after as a second argument this can make for some quite
irksome bookkeeping. (Remember our little XML parser plus treeview in Section
7.4? Well, this is the cause of one nasty bug in that code! In that treeview, all items
are sorted within their node, and thus do not represent the structure of the XML
document.)
211
Chapter 10. Qt Class Hierarchy
10.6.5. QSplitter
QSplitter is used to separate two gui items that can take a variable amount of
space. The user can drag the splitter to give more room to one of those items.
Splitters can be horizontal or vertical, and you can use splitters within splitters.
QCanvasSprite
QCanvasText
QCanvasPolygonalItem
QCanvasEllipse
QCanvasLine
QCanvasPolygon
QCanvasRectangle
From Qt 3, there is also QCanvasSpline, which can be used to draw bezier curves.
Note that you cannot subclass QCanvasItem this is explicitly forbidden in the
Qt documentation: you will have to select a more specialized subclass of
QCanvasItem.
Canvas items can move independently from each other, and can be rendered on top
of other items, or below others (by clipping the obscured part). The PyQt canvas is
completely double-buffered and thus gives a very smooth performance.
Section 21.2 shows a practical use for a QCanvas.
212
Chapter 10. Qt Class Hierarchy
213
Chapter 10. Qt Class Hierarchy
Too large...
Too small.
Its easy to write applications as badly behaved as this in PyQt but where a Visual
Basic developer has to write a complex resize routine that recalculates the size and
position of each element, PyQt developers can use Qts advanced layout
management facilities.
Basically, this means that you create several containers that hold your widgets, and
those widgets will resize together with the containers. The easiest way to create a
pleasing layout is by using the BlackAdder or Qt forms designer, as this
automatically uses sensible defaults.
There are three fundamental approaches to layout management in PyQt: by stacking
widgets or grouping them in frames, by using the simple layout management
provided by QFrame and children, or by using the advanced layout management
214
Chapter 10. Qt Class Hierarchy
QLayout provides. In Qt 3.0 QLayout is even smart enough to reverse the order of
labels and entry widgets in dialog boxes for right-to-left scripts.
215
Chapter 10. Qt Class Hierarchy
10.7.2.1. QHBox
This a very, very simple class. A QHBox aligns its children horizontally, with a
settable spacing between them.
10.7.2.2. QVBox
A QVBox layout is possibly even simpler than the QHBox layout: as the name
implies, it aligns its children vertically.
10.7.2.3. QGrid
It will come as no surprise that the QGrid is a simple grid layout manager for
more complicated layouts, with differently sized columns, you really need
QGridLayout.
216
Chapter 10. Qt Class Hierarchy
10.7.2.4. QGroupBox
A QGroupBox gives you a frame (which can be drawn in as many flavors as
QFrame supports) and with a title text which will appear on top of the frame. A
QGroupBox can hold child widgets. Those widgets will be aligned horizontally,
vertically or in a grid. The grid can also be filled in columns (for vertically oriented
frames), or in strips (for horizontally oriented frames).
10.7.3. QLayout
QLayout is foundation of all complex Qt layout managers. Built on QLayout, there
are three layoutmanagers: one for horizontal layouts, one for vertical layouts and
one for grids. Its also quite possible to build a new layoutmanager on QLayout,
one, for instance, that manages playing cards on a stack, or perhaps items in circle.
You can not only add widgets to layouts; but also layouts. In this way, quite
complex layouts can achieved with very little pain.
All layout managers work by maintaining a list of layoutitems. Those items can be
QLayoutItems, QLayoutWidgets or QSpacerItems. Its interesting to note that
QLayout is a QLayoutItem, and can thus be managed by another layoutmanager.
Every layout item proxies for a widget. A QSpacerItem is rather special, since it
doesnt represent a widget, but rather space. A QSpacerItem pushes other
widgets, either horizontally or vertically. You can use them to push all pushbuttons
to the top of the dialog, instead of having them spread out over the whole height by
the layout manager.
217
Chapter 10. Qt Class Hierarchy
10.7.5. QGridLayout
While you can handle many layout problems with combinations of horizontal and
vertical box layout managers, other problems are more suited for a grid-based
layout. The QGridLayout class provides a flexible layout manager.
The grid managed by a QGridLayout consists of cells laid out in rows and
columns: the grid cannot be as complicated as a html table, but you can add widgets
(or sub-layouts) that span multiple rows and columns. Rows (or columns) can be
given a stretch factor and spacing.
Example 10-12. layout.py - two box layouts and adding and removing buttons
dynamically to a layout
#
# layout.py - adding and removing widgets to a layout
#
import sys
from qt import *
class MainWindow(QMainWindow):
self.bnAdd=QPushButton("Add wid-
get", self.mainWidget, "add")
self.connect(self.bnAdd, SIGNAL("clicked()"),
self.slotAddWidget)
self.bnRemove=QPushButton("Remove widget",
218
Chapter 10. Qt Class Hierarchy
self.mainWidget, "remove")
self.connect(self.bnRemove, SIGNAL("clicked()"),
self.slotRemoveWidget)
self.buttonLayout.addWidget(self.bnAdd)
self.buttonLayout.addWidget(self.bnRemove)
self.buttons = []
def slotAddWidget(self):
widget=QPushButton("test", self.mainWidget)
self.widgetLayout.addWidget(widget)
self.buttons.append(widget)
widget.show()
def slotRemoveWidget(self):
self.widgetLayout.parent().removeChild(self.widgetLayout)
self.widgetLayout=QVBoxLayout(self.mainLayout, 5, "widget")
self.buttons[-1].parent().removeChild(self.buttons[-
1])
del self.buttons[-1:]
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
This example shows that it is not only possible to dynamically add widgets to a
layout, but also to remove them again. Removing means first severing the link from
the widget to the parent, and then deleting (using del) all Python references to the
widget. When the last reference has been removed, the widget disappears.
219
Chapter 10. Qt Class Hierarchy
layout.py
10.7.6. setGeometry
You can use setGeometry to set the size of every individual widget yourself.
Theres another useful application of setGeometry(), too: if you save the size of
the application window when the last window closes in a configuration file, you can
bring the window back to its last size and position the next time the user starts
opens it.
#
# geometry.py
#
import sys
from qt import *
class MainWindow(QMainWindow):
def main(args):
app=QApplication(args)
win=MainWindow()
win.setGeometry(100,100,300,300)
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
220
Chapter 10. Qt Class Hierarchy
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
10.8.1. QDialog
QDialog is the parent of all dialog classes. A dialog window is a window that pops
up over the application window. These can be modal (where it will block the rest of
the application) or modeless (where the user can continue working in the main
screen of the application). Dialogs are commonly closed with OK or Cancel
buttons. There is no reason to make a dialog a fixed size; you can give it a
QSizeGrip, and if you use QLayout layout management, the contents will be
resized quite nicely. A modal dialog has its own exec_loop; a modeless dialog can
be constructed, shown, and hidden, but is part of its parents event loop.
Of course, there are many other occasions where you will want to create custom
dialog boxes. PyQt provides for plain dialog boxes, expanding dialog boxes, tabbed
dialog boxes and wizards.
221
Chapter 10. Qt Class Hierarchy
10.8.2. QMessageBox
A QMessageBox is a very simple standard dialog class. Message boxes are always
modal, and can be used to inform, warn or frighten the user. Message texts should
preferably short, specific, and as non-threatening as possible.
#
# dialogs.py
#
import sys
from qt import *
class MainWindow(QMainWindow):
self.actionInformation=QAction(self, "Information")
self.actionInformation.setText("Informational Message")
self.actionInformation.setMenuText("&Information")
self.actionInformation.setStatusTip("Show an informa-
tional mesagebox.")
self.connect(self.actionInformation,
SIGNAL("activated()"),
self.slotInformation)
self.actionWarning=QAction(self, "Warning")
self.actionWarning.setText("Warning Message")
self.actionWarning.setMenuText("&Warning")
self.actionWarning.setStatusTip("Show a warn-
ing mesagebox.")
self.connect(self.actionWarning,
SIGNAL("activated()"),
self.slotWarning)
222
Chapter 10. Qt Class Hierarchy
self.actionCritical=QAction(self, "Critical")
self.actionCritical.setText("Critical Message")
self.actionCritical.setMenuText("&Critical")
self.actionCritical.setStatusTip("Show an informa-
tional mesagebox.")
self.connect(self.actionCritical,
SIGNAL("activated()"),
self.slotCritical)
self.actionAbout=QAction(self, "About")
self.actionAbout.setText("About")
self.actionAbout.setMenuText("&About")
self.actionAbout.setStatusTip("Show an about box.")
self.connect(self.actionAbout,
SIGNAL("activated()"),
self.slotAbout)
self.actionAboutQt=QAction(self, "AboutQt")
self.actionAboutQt.setText("About Qt Message")
self.actionAboutQt.setMenuText("About &Qt")
self.actionAboutQt.setStatusTip("Show an about box for Qt.")
self.connect(self.actionAboutQt,
SIGNAL("activated()"),
self.slotAboutQt)
self.actionFile=QAction(self, "OpenFile")
self.actionFile.setText("Open File")
self.actionFile.setMenuText("&Open")
self.actionFile.setStatusTip("Open a file.")
self.connect(self.actionFile,
SIGNAL("activated()"),
self.slotFile)
223
Chapter 10. Qt Class Hierarchy
self.actionFont=QAction(self, "Font")
self.actionFont.setText("Select a font")
self.actionFont.setMenuText("&Font")
self.actionFont.setStatusTip("Select a font")
self.connect(self.actionFont,
SIGNAL("activated()"),
self.slotFont)
self.actionColor=QAction(self, "Color")
self.actionColor.setText("Select a color")
self.actionColor.setMenuText("&Color")
self.actionColor.setStatusTip("Select a color")
self.connect(self.actionColor,
SIGNAL("activated()"),
self.slotColor)
# Statusbar
self.statusBar=QStatusBar(self)
# Define menu
self.messageMenu=QPopupMenu()
self.actionInformation.addTo(self.messageMenu)
self.actionWarning.addTo(self.messageMenu)
self.actionCritical.addTo(self.messageMenu)
self.dialogMenu=QPopupMenu()
self.actionFile.addTo(self.dialogMenu)
self.actionFont.addTo(self.dialogMenu)
self.actionColor.addTo(self.dialogMenu)
self.helpMenu=QPopupMenu()
224
Chapter 10. Qt Class Hierarchy
self.actionAbout.addTo(self.helpMenu)
self.actionAboutQt.addTo(self.helpMenu)
self.menuBar().insertItem("&Messages", self.messageMenu)
self.menuBar().insertItem("&Standard di-
alogs", self.dialogMenu)
self.menuBar().insertItem("&Help", self.helpMenu)
def slotInformation(self):
QMessageBox.information(self,
"Information",
"A plain, informa-
tional message")
def slotWarning(self):
QMessageBox.warning(self,
"Warning",
"What you are about to do will do some se-
rious harm .")
def slotCritical(self):
QMessageBox.critical(self,
"Critical",
"A critical error has oc-
curred.\nProcessing will be stopped!")
def slotAbout(self):
QMessageBox.about(self,
"About me",
"A demo of message boxes and stan-
dard dialogs.")
def slotAboutQt(self):
QMessageBox.aboutQt(self)
def slotFile(self):
file-
name=QFileDialog.getOpenFileName("", "*.py", self, "FileDialog")
225
Chapter 10. Qt Class Hierarchy
def slotFont(self):
(font, ok) = QFontDialog.getFont(self, "FontDialog")
def slotColor(self):
color=QColorDialog.getColor(QColor("linen"), self, "ColorDialog")
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()")
, app
, SLOT("quit()")
)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
A gentle warning
A dire warning
226
Chapter 10. Qt Class Hierarchy
About Qt
10.8.3. QTabDialog
One of the best ways to organize a multitude of options is to group them together
and show the user only the pertinent set, hiding the rest between tabs. Usability
studies have shown that a moderate number of tabs, presented in a single row
showing all available tabs at one time, promotes the greatest usability. Twenty tabs
in three rows confuse the user; one scrolling row of twenty tabs irritates the user. I
have once used tabs within tabs myself, but its not something Id recommend.
10.8.4. QWizard
Complex, infrequent actions are eminently suited to the wizard approach. A wizard
is a set of pages that guide the user through a certain path. The user need not visit
all pages, and there might be more than one possible path. Avoid using wizards
where tab pages might be more suited (when there are many options but no clear
progression through the steps of a complex action).
10.8.5. QFileDialog
The first of the Qt standard dialogs is the QFileDialog. The file dialog can be
227
Chapter 10. Qt Class Hierarchy
extended with custom icons, toolbuttons and extra widgets. In its default format it is
extremely easy to use: just call one of the predefined class methods that return the
name of a directory or file, such as getOpenFileName() or
getOpenFileNames() .
...
def slotFile(self):
file-
name=QFileDialog.getOpenFileName("", "*.py", self, "FileDialog")
...
10.8.6. QFontDialog
A useful dialog, QFontDialog lets the user select a font by giving parameters for
font name, style, size, effects and script this last parameter being the encoding of
the font, such as Unicode. Just as with QFileDialog, QFontDialog provides a set
of class methods that return the selected value, in this case a tuple containing a
QFont object and a boolean value that indicates whether OK or Cancel was
pressed..
Of course, with Qt3, you no longer set the desired encoding, but rather the script -
Greek, Tamil, or whatever you want.
228
Chapter 10. Qt Class Hierarchy
...
def slotFont(self):
(font, ok) = QFontDialog.getFont(self, "FontDialog")
...
10.8.7. QColorDialog
QColorDialog provides a standard dialog for color selection. An interesting
addition to this class is that you ask it to store a set of custom colors. This set will
be kept during the lifetime of the application, and you can store those colors in a
configuration file and restore them when the app is restarted. You can ask the color
dialog either for a QColor object, or for a set of RGB values, encapsulated in a
QRgb object. In contrast with QFileDialog, which is extensible, or
QFontDialog, which really suffices, QColorDialog provides just barely enough
for simple color selection, but wont do for more complex graphics applications
(with which you might want to implement something that works with HSV values,
or with a color wheel).
...
229
Chapter 10. Qt Class Hierarchy
def slotColor(self):
color=QColorDialog.getColor(QColor("linen"), self, "ColorDialog")
...
10.8.8. QInputDialog
You can use QInputDialog to ask the user for a simple, single value. This value
can be of the following type: text, integer, double, or an item from a listbox.
Frankly, Ive never had a need for these. The open remote location dialog in
browsers like Opera or Netscape are a common example.
10.8.9. QProgressDialog
The QProgressDialog is a useful little dialog that can be used to inform the user
that a certain action will be taking a lot of time. If the operation of the dialog is
meant to block the whole application, use a modal QprogressDialog . If the
operation wont block the entire application, then its possible to use a modeless
QProgressDialog , but it may be more effective to use a QProgressBar in the
statusbar of the application. QProgressDialog is based on the QSemiModal class.
230
Chapter 10. Qt Class Hierarchy
#!/usr/bin/env python
import sys
from qt import *
class Directory(QListViewItem):
def __init__(self, parent, name=None):
apply(QListViewItem.__init__,(self,parent))
if isinstance(parent, QListView):
self.p = None
self.f = /
else:
self.p = parent
self.f = name
231
Chapter 10. Qt Class Hierarchy
self.c = []
self.readable = 1
files = thisDir.entryInfoList()
if files:
for f in files:
fileName = str(f.fileName())
if fileName == . or fileName == ..:
continue
elif f.isSymLink():
d = QListViewItem(self, fileName, Symbolic Link)
elif f.isDir():
d = Directory(self, fileName)
else:
if f.isFile():
t = File
else:
t = Special
d = QListViewItem(self, fileName, t)
self.c.append(d)
QListViewItem.setOpen(self, o)
def setup(self):
self.setExpandable(1)
QListViewItem.setup(self)
def fullName(self):
if self.p:
s = self.p.fullName() + self.f + /
else:
s = /
return s
232
Chapter 10. Qt Class Hierarchy
a = QApplication(sys.argv)
mw = QListView()
a.setMainWidget(mw)
mw.setCaption(Directory Browser)
mw.addColumn(Name)
mw.addColumn(Type)
mw.resize(400, 400)
mw.setTreeStepSize(20)
root = Directory(mw)
root.setOpen(1)
mw.show()
a.exec_loop()
...
def setOpen(self, o):
if o and not self.childCount():
s = self.fullName()
if (not os.path.isdir(s)):
self.readable == 0
return
files=os.listdir(s)
if files:
233
Chapter 10. Qt Class Hierarchy
if os.path.isfile(f):
t = File
else:
print f
t = Special
d = QListViewItem(self, fileName, t)
self.c.append(d)
QListViewItem.setOpen(self, o)
...
The first snippet has been taken from the dirview.py example script that comes with
PyQt and BlackAdder; the second is my own interpretation of the dirview.cpp
example application. Both have been slightly adapted to make them more alike.
234
Chapter 10. Qt Class Hierarchy
Perhaps the similarities between the Qt QDir object and the Python os.path
module are even more striking than the differences.
235
Chapter 10. Qt Class Hierarchy
236
Chapter 10. Qt Class Hierarchy
237
Chapter 10. Qt Class Hierarchy
Python and caching: Python caches certain often-used values and shares
those values across variables. Numbers from 0 to 99 (inclusive) are cached,
and strings are always cached. Qt uses the same trick from strings and some
other objects
238
Chapter 10. Qt Class Hierarchy
239
Chapter 10. Qt Class Hierarchy
While Python has a time module, this only presents a low-level interface to the
system date and time functions (and the sleep() function, which halts processing
for a while). It does not provide a high-level encapsulation of dates and times. PyQt,
however, provides just that with QDate and QTime. QDate is especially suited to
data arithmetic, and can hold dates from 1752 to 8000. The first limit is based on
the date that our Gregorian calendar was introduced; if you need the Julian calendar
(or perhaps Vikram Samvat or other exotic calendars), you must write your own
Date class.
10.9.4. Mime
Python mimetools and the MimeWriter modules are not exactly equivalent to the
PyQt QMimeSource and QMimeSourceFactory classes. The Python modules are
optimized for the handling of mime-encoded e-mail messages. The PyQt classes are
a more generalized abstraction of formatted data, where the format is identified by
the IANA list of MIME media types. QMimeSource is used extensively in the
240
Chapter 10. Qt Class Hierarchy
...
fileOpenText = \
<img source="fileopen">
Click this button to open a <em>new file</em>.<br><br>
You can also select the <b>Open</b> com-
mand from the <b>File</b> menu.
...
QWhatsThis.add(self.fileOpen,fileOpenText)
QMimeSourceFactory.defaultFactory().setPixmap(fileopen,openIcon)
...
241
Chapter 10. Qt Class Hierarchy
Qts string handling really excels: it is thoroughly based upon Unicode, but provides
easy functionality for other character encodings. However, plain Python also
provides Unicode string functionality, and the interplay between Python strings and
PyQt QStrings can be quite complex.
For most purposes, however, the conversions are transparent, and you can use
Python strings as parameters in any function call where a QString is expected. If
you run across more complex problems, you can consult Chapter 8, on String
Objects in Python and Qt.
10.9.6. Threads
QMutex
Python threads and Qt threads bite each other frequently. Qt thread support itself is
still experimental, and with Unix/X11, most people still use the un-threaded Qt
library. The C++ Qt thread class QMutex has not been ported to PyQt, so you
cannot serialize access to gui features.
Python thread support is far more mature, but doesnt mix too well with PyQt
you dont want two threads accessing the same gui element. Youre quite safe
though, as long as your threads dont access the gui. The next example shows a
simple pure-python script with two threads:
#
# thread1.py
#
import sys
import time
from threading import *
class TextThread(Thread):
242
Chapter 10. Qt Class Hierarchy
def run(self):
while self.counter < 200:
print self.name, self.counter
self.counter = self.counter + 1
time.sleep(1)
def main(args):
thread1=TextThread("thread1")
thread2=TextThread("thread2")
thread1.start()
thread2.start()
if __name__=="__main__":
main(sys.argv)
The next example has a Qt window. The threads run quite apart from the window,
and yet everything operates fine that is, until you try to close the application. The
threads will continue to run until they are finished, but it would be better to kill the
threads when the last window is closed. Killing or stopping threads from outside the
threads is not supported in Python, but you can create a global variable, stop, to
circumvent this. In the threads themselves, you then check whether stop is true.
#
# thread2.py - Python threads
#
import sys, time
from threading import *
from qt import *
class TextThread(Thread):
243
Chapter 10. Qt Class Hierarchy
def run(self):
while self.counter < 200:
print self.name, self.counter
self.counter = self.counter + 1
time.sleep(1)
class MainWindow(QMainWindow):
def main(args):
app=QApplication(args)
win=MainWindow()
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"),
app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
10.9.7. URLs
The Qt URL-handling classes are quite equivalent to Pythons urllib module. Its up
to you to choose which you prefer.
244
Chapter 10. Qt Class Hierarchy
QUrl
QUrlInfo
QUrlOperator
245
Chapter 10. Qt Class Hierarchy
246
Chapter 10. Qt Class Hierarchy
247
Chapter 10. Qt Class Hierarchy
248
Chapter 11. Qt Designer, BlackAdder
and uic
BlackAdder is the most powerful Python GUI designer in existence. In fact, it
compares favorably with every other GUI designer I have ever used. There are other
GUI designers for Python, notably Pythonworks by Secret Labs and Boa
Constructor, but Pythonworks gives you access to a only subset of the relatively
feeble Tkinter GUI toolkit, and Boa Constructor, for wxWindows, is not integrated
into a development environment.
With BlackAdders GUI designer you can create dialog windows, custom widgets
and wizards. In the next generation of BlackAdder, which will be based on Qt 3.0,
you can even create complete main windows with menus, toolbars and a main
widget. BlackAdder gives you access to a wide range of widgets, and makes it
possible to integrate your own widgets.
Note that everything mentioned in this chapter holds equally true for Qt Designer.
The combination of Qt, Qt Designer, pyuic and PyQt gives you exactly the same
power just not the same convenience.
There are a number of unique features to the GUI designer in BlackAdder:
The designer produces XML files that can be compiled to Python or C++.
You can create signal/slot connections in the designer, thus tying together all
aspects of interface logic.
You can use the layout management classes of Qt (like QLayout).
You can preview your work in any of the native styles that Qt supports.
You can add your own widgets even if they are written in Python instead of
C++
11.1. Introduction
Working with the designer modules includes creating files with your interface
definition, compiling those files to Python code, and then using that code in your
249
Chapter 11. Qt Designer, BlackAdder and uic
application.
This dialog should be moderately familiar to developers who have worked with
other GUI designers, such as Visual Basic and JBuilder. Currently, the available
options are:
Dialog
Wizard
Widget
Configuration Dialog
Dialog with Buttons (bottom)
Dialog with Buttons (right)
Tab-Dialog
Adding templates: You are not limited to these choices the list is infinitely
extensible, because all valid designer files (those ending in .ui) are also valid
templates for the designer. You can create a new template using the Designer,
and then copy the .ui file to the templates directory in the
BlackAdder/properties directory. The next time you want to create a
250
Chapter 11. Qt Designer, BlackAdder and uic
designer file, your template will be among the choices. Of the original choices,
Configuration Dialog, Dialog with Buttons (Bottom), Dialog with Buttons
(Right) and Tab Dialog are based on .ui files, and are therefore customizable.
Dialog is relatively uninteresting. It is a base class for creating modal and modeless
dialog boxes.
Wizard is more interesting. This template, based on QWizard, offers everything you
need to create these popular "hand-holding" forms.
251
Chapter 11. Qt Designer, BlackAdder and uic
The dialogs with buttons to the right or to the bottom are useful, everyday dialogs.
The included buttons are already connected to the methods that close and cancel the
dialog, and the contents are already subject to layout management. Which
constellation you prefer is a matter of taste. For instance, the KDE desktop standard
calls for buttons at the bottom; but Microsoft often puts the buttons that right-hand
side.
The last default template is for creating a bottom-buttoned dialog with a tab strip on
top.
252
Chapter 11. Qt Designer, BlackAdder and uic
253
Chapter 11. Qt Designer, BlackAdder and uic
One example is the groupbox around a set of radio buttons. It is essential to create
the radio buttons inside the groupbox to make them a set; otherwise it would be
difficult to keep the selection unique. Thus, you first create the groupbox, drag it to
an agreeable size, and then place the radiobuttons inside the groupbox.
The toolbar buttons for the layout managers set size,: horizontal, vertical, grid,
break layout and add a spring.
Layout management can be further augmented by adding size hints to each widget.
These hints determine whether the widget should stretch as much as possible, or
stay the same size.
254
Chapter 11. Qt Designer, BlackAdder and uic
Selecting a buddy.
BlackAdder can check for duplicate accelerators. In the Edit menu, select the option
Check Accelerators. Theres a shortcut for this, too: CTRL-R .
Defining accelerators is one part of creating a GUI that is usable with the keyboard
only. The tab order is important, too. If the user presses the Tab key, the focus
should shift to the next widget (from left to right), instead of going hoppity-skip all
over the form.
Therefore, fixing the tab order should be the last thing you do after completing a
form. This is very easy: press the right button on the toolbar, or choose Tools Tab
Order (shortcut: F4). BlackAdder then superimposes small numbered circles on
every widget. You simply click on these widgets in the order you want the focus to
follow, and BlackAdder does the rest. Life could not be more simple!
255
Chapter 11. Qt Designer, BlackAdder and uic
Setting the tab order right now becomes one of those pleasurable little tasks that
give a developer a bit of thinking time.
256
Chapter 11. Qt Designer, BlackAdder and uic
The first step is to create a design. Based on the DIalog with Buttons (right)
template, we add two QListBoxes and four buttons:
Initial design.
If you right-click on the form, and then choose Connections, you will see that
there are already two connections made, namely between the OK button and the
Cancel button. It is our task to create more connections.
The goal of the buttons is to move items from the left listbox to the right listbox,
and back. Double-arrowed buttons move everything, and single-arrowed buttons
move the selection. Select the connection button ( ), and draw a line from the
top button to any place on the form. A dialog pops up that lets you select from the
signals of the button and the slots of the form. However, there is no slot available
that says something useful like slotAddAllFromLeftToRight() !
Does this mean that you are restricted to the slots as defined in the PyQt library?
Fortunately, no. You can add your own slots but only to the form, not to the
257
Chapter 11. Qt Designer, BlackAdder and uic
individual widgets. This is actually quite logical; later, you will generate a Python
class from the .ui design. You then subclass the generated Python code to add
functionality. Since you will only subclass the form, the form is the only place you
will be able to add slots. If you want custom slots in your widgets, you will have to
add custom widgets to Designer.
Your subclass will be a descendant of the entire form, so you can only add
functionality to the form, not to the widgets. Of course, you can also create custom
widgets with custom signals and slots, and use those instead of the standard
QListBox. I will discuss the technique for adding custom widgets in the next
section.
Lets go ahead and add our custom slots to the form. This is quite easy. Select the
Slots menu item from the Edit menu, and press the New Slot button. Now you can
edit the text in the Slot Properties text field. Type the name of the slot, and then
enter the types of the arguments the slot should be called with, between brackets.
This is not useful in our case, since we will call the slots with the clicked()
signal of the buttons, and these dont pass on an argument.
Define the following four slots:
slotAddAll()
slotAddSelection()
slotRemoveAll()
slotRemoveSelection()
258
Chapter 11. Qt Designer, BlackAdder and uic
Now, you can connect the clicked() signal of each button to the right slot.
The Access specifier in the slot definition dialog is only important if you want to
migrate your designs to C++ at some time. "Public" means that all classes in your
C++ program have access to those slots; protected means that only the generated
class itself and its subclasses can access the slot. Protected is as if the slotname
were prefixed with a double underscore in Python.
259
Chapter 11. Qt Designer, BlackAdder and uic
Choose Compile Form from the File menu. This will generate a Python file that
implements your design. For now, this is enough. As I will show later, you should
subclass the generated Python file and add some real logic, and perhaps a few
signals.
For now, we have a custom component, designed with BlackAdder and
implemented in Python. This component we will add to the BlackAdder
components palette, and use it in a dialog.
Choose Edit Custom Widgets from the Custom submenu in the Tools menu. This
will open a rather complicated dialog that lets you add new widgets.
260
Chapter 11. Qt Designer, BlackAdder and uic
You must type the name of the Python class in this case DoubleListBox in
the Class text field. The headerfile text field refers ostensibly to a C++ header file;
but BlackAdder assumes that it refers to a Python file. Enter wdglistbox, if that is
the name you saved your custom widget under. Do not add the extension. The
choice between local and global only has a meaning for C++ and defines the type of
include.
The rest of the fields are less vital. You can create a pixmap that represents your
widget; if you dont, the green Trolltech logo will take be used a placeholder. You
can give a default size hint and size policy. For example, if you want the double
listbox to take as much space as it can get, set both policies to expanding. Our
double listbox cannot contain other widgets (it is not like a groupbox), and therefore
we dont check the Container Widget checkbox.
In the other tabs, we can enter the slots and signals that our widget knows; this is
only useful for slots and widgets that have a meaning to the outside world. The four
special slots defined in the previous section are for internal use. In a subclass of
DoubleListBox, we might define a few extra signals, like:
sigItemAdded(ListViewItem)
sigItemDeleted(ListViewItem)
261
Chapter 11. Qt Designer, BlackAdder and uic
If you press OK a new item will be added to the toolbars, which you can select and
put on a form. If you do so, you will see that the icon is also used to represent the
widget on the form, instead of a more faithful representation of the widget. When
you preview the form, you wont see the widget either; but wen you generate the
form, everything will be all right.
262
Chapter 11. Qt Designer, BlackAdder and uic
that for you, if only you let Qt manage your layouts. The same goes for the size of
labels. If you pixel-position your controls to the width of the labels, then there
wont be room for languages that use fichier for file, or annuleren for cancel.
All these arguments have never before swayed developers to use automatic layout
management, but with PyQt and BlackAdder, layout management is ridiculously
easy (and certainly easier than manual layout). This, at least, should convert the
developing masses to automatic layouting!
The Designer module of BlackAdder offers three layout managers, and a helpful
tool called the spacer. The layout managers are:
horizontal
vertical
grid
By nesting layouts, together with the spacer and the sizepolicies and sizehints of
each individual widget, you can create almost any layout. A good rule of thumb is
perhaps that if your intended layout confuses the layout managers of the Designer,
then it will probably also confuse your users.
A good layout is one that can be easily taken in with one look, and that neatly
groups the various bits of data in the form. A good layout will also be simple
enough that the form wont take an eternity to appear. Bear in mind that Python has
to load the form, lay it out, and, most importantly, fill the various fields with
relevant the data. The last step can take a lot of time. I once had to create a form that
brought together about sixty pieces of information from more than twenty database
tables. My client was not pleased when this form wouldnt appear in the required
three seconds.
Ive already discussed the classes behind the horizontal, vertical and grid layout
managers: QLayout, QBoxLayout, QVBoxLayout, QHBoxLayout and
QGridLayout.
You can influence the layouts by selecting them in the object hierarchy window.
Interesting properties include LayoutSpacing and LayoutMargin. The first
determines how much room there is between widgets; the second determines how
much space the layout wants between itself and the border of the window or other
layouts.
263
Chapter 11. Qt Designer, BlackAdder and uic
264
Chapter 11. Qt Designer, BlackAdder and uic
want everything in your form to be managed in a grid, then you can simply select
this layout manager from the toolbar, and then click somewhere on the background
of the form. The Designer module is very clever, and will try to retain your current,
manually created layout as far as possible. It can even create the difficult
multi-column widgets automatically.
265
Chapter 11. Qt Designer, BlackAdder and uic
the tendency of the listbox to usurp all the space in the dialog, you should set its
maximum width to something sensible. Note also that, alas, the layout management
of the forms in the designer doesnt work exactly the same as the layout
management of the running forms. You can see the difference in the preview mode.
The sizepolicy works in concord with the result of calls to the sizeHint ()
function this function returns the size the widget wants to be, and the
minimumSizeHint() function, which returns the absolute minimum size the
widget can be. The following hints can be used for setting the sizepolicy of widgets:
266
Chapter 11. Qt Designer, BlackAdder and uic
267
Chapter 11. Qt Designer, BlackAdder and uic
The result should be quite pleasing take a look at how Designer created the final
grid layout. Perhaps it would be better to encase the checkboxes in a groupbox, too,
but this is not essential. Some GUI design guidelines urge you to envelop everything
but the OK and Cancel buttons (and perhaps the Help button if its in the same row
or column) in a frame with a title. Personally, Im in favor of that recommendation,
but in this you may follow the dictates of your heart (or of your primary platform).
Ultimately, the layout management offered by the Designer is useful and sufficient
for most cases; in certain cases you might want to experiment with coding the layout
management yourself. This is a lot more flexible, but it takes a lot more time, too.
#
# dglcomplex.py
#
import sys
from qt import *
268
Chapter 11. Qt Designer, BlackAdder and uic
def accept(self):
print "OK is pressed"
FrmComplex.accept(self)
def reject(self):
print "Cancel pressed"
QDialog.reject(self)
if __name__ == __main__:
a = QApplication(sys.argv)
QObject.connect(a,SIGNAL(lastWindowClosed()),a,SLOT(quit()))
w = DlgComplex()
a.setMainWidget(w)
w.show()
a.exec_loop()
269
Chapter 11. Qt Designer, BlackAdder and uic
Because the generated code in FrmComplex doesnt really add anything, calling
QDialog.reject() works just as well.
The next move is extending the constructor of the derived class to set the initial
values of the various widgets.
self.ListBox2.insertItem("Thats a turnip")
self.ListBox2.insertItem("Further nonsense")
self.RadioButton1.setChecked(1)
As you can see, its simply a matter of remembering what names you gave each
widget, and inserting stuff no rocket science here.
Accessing the values of each widget after the user has pressed OK is just as easy. A
dialog may disappear from screen when the user presses OK, but that does not mean
that the dialog has disappeared from memory. As long as there is a variable that
points to the dialog, you can access each and every field.
270
Chapter 11. Qt Designer, BlackAdder and uic
271
Chapter 11. Qt Designer, BlackAdder and uic
272
III. Creating real
applications with PyQt
Table of Contents
12. Application Frameworks...............................................................................275
13. Actions: menus, toolbars and accelerators ..................................................287
14. Automatic testing with PyUnit......................................................................297
15. A More Complex Framework: Multiple Documents, Multiple Views ......315
16. User Interface Paradigms..............................................................................349
17. Creating Application Functionality..............................................................363
18. Application Configuration.............................................................................379
19. Using Dialog Windows...................................................................................399
20. A Macro Language for Kalam ......................................................................437
21. Drawing on Painters and Canvases ..............................................................461
22. Gui Design in the Baroque Age.....................................................................485
23. Drag and drop ................................................................................................521
24. Printing ...........................................................................................................527
25. Internationalizing an Application.................................................................533
26. Delivering your Application ..........................................................................541
27. Envoi................................................................................................................553
The last part describes the complete development of an application from the
architectural beginnings, to the final release as an installable package. Along the
way we will visit the most remote corners of the PyQt library although we wont
use every single widget that PyQt offers us.
273
Chapter 11. Qt Designer, BlackAdder and uic
274
Chapter 12. Application Frameworks
Up to this point we have seen only example scripts Exciting examples,
illuminating examples, promising examples, but still just examples. Example scripts
are far removed from the realities of a complex GUI application. For a complex
application you need a well thought-out modular structure, where each component
can find its place. You need an architecture, and you need a design.
Most books on programming languages dont progress much beyond basic
examples; indeed, it is not really possible to discuss a complete, complex
application. Still, in this part of the book I want to show how BlackAdder and PyQt
can help you achieve a well-written, maintainable application, starting with the
architecture, and then moving on to the outlines of an application. On the way, Ill
show you one useful way of laying out the project structure. In the next few
chapters well build this framework into a real application.
275
Chapter 12. Application Frameworks
The component that represents the data is variously termed model or document; the
component that actually shows the data on screen is the view. The
model-view-controller framework adds a controller component, which represents
the user input.
The controller component receives mouse clicks, key press events and all other user
input, and passes those on to the model. The model determines its current state from
that input, and notifies the view that its representation of the model should be
changed. Sounds like PyQt signals and slots would come in handy, doesnt it?
Model-view-controller architecture
Be aware of the fractal nature of this architecture. You can envision your entire
application divided into two or three parts one component for the model, one for
the view, and perhaps one for the controller. However, the same tripartition can be
designed for the lowliest checkbox class. Here, the boolean value is the model, the
picture of a box with a cross in it is the view, and the event handler is the controller.
Swing, the Java gui toolkit, does exactly this, and gives you the opportunity to write
specialized models and controllers for almost all its widgets (and specialized views,
too). PyQt doesnt go quite that far, and its widgets are based on a simpler, more
monolithic model. Like all good ideas carried through to their extremes, writing
models and controllers for every widget is a bit tiresome. Thats why Javas Swing
also presents capable default implementations for the controller and model parts.
This chapter is about application architecture, and when speaking of views and
models, documents and controllers, I do so only at the application architecture level,
276
Chapter 12. Application Frameworks
not the widget level. However, a complex application could consist of several
models and views: for instance, in an application based on a database, you could
view every table as a model and every corresponding form as a view.
There must be an interface between the document and the view. Changes made in
the view must be passed on to the document, and vice-versa. A simple
document-view framework is readily constructed:
The basic application structure consists of three classes: an application class, a view
class and a document class. In the next few chapters of this part, well work with the
framework to build a real application. Well also extend it to handle multiple
document windows: the framework detailed below can only work with one
document. The complete framework is in the file docview.py.
class DocviewDoc(QObject):
277
Chapter 12. Application Frameworks
apply(QObject.__init__, (self,)+args)
self.modified=FALSE
def slotModify(self):
self.modified = not self.modified
self.emit(PYSIGNAL("sigDocModified"),
(self.modified,))
def isModified(self):
return self.modified
You should always begin with designing the application model - or so the theory
goes. Your preferences might lie with first creating a mock-up of the interface using
generic widgets, in order to be able to have something concrete to talk about. Thats
fine with me. Anyway, the DocviewDoc class represents the document or the
application model. This can be as complex as you want. This class merely
remembers whether it has been modified. The controlling application can query the
document using the isModified() function to determine whether the document
has changed, and it can hook a QAction to the slotModify() slot to signal user
interaction to the model. Separating all code that handles the application data makes
it easy to write automated tests using Pyunit. This is the topic of the next chapter.
DocviewView is the view class in the framework. A view is a visual component; in
PyQt it must somehow descend from QWidget either directly, as it is done here,
or via a more specialized class, such as QTable or QCanvas. A reference to the
application model is passed to the view. This breaks encapsulation somewhat, but it
makes initially setting up the display a lot easier.
Warning
I mentioned earlier, in Section 10.4.1, that the nice people at
Trolltech changed the name of the function that is used to set
background colors from setBackgroundColor to
setEraseColor. This means of course that you, if you want to
run this example with PyQt 3, will have to adapt the relevant
calls.
class DocviewView(QWidget):
278
Chapter 12. Application Frameworks
The document has to notify the view of changes. This means that the view has to
have slots corresponding to all the document signals the view is interested in. A
view can thus show changes to the document selectively, and you can create more
than one view, each with a specialized function.
The DocviewApp is the controller component. It controls both view and document.
class DocviewApp(QMainWindow):
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
self.initActions()
self.initMenuBar()
self.initToolBar()
self.initStatusBar()
self.initDoc()
self.initView()
def initActions(self):
fileQuitIcon=QIconSet(QPixmap(filequit))
279
Chapter 12. Application Frameworks
self.actions = {}
self.actions["fileQuit"] = QAction("Exit",
fileQuitIcon,
"E&xit",
QAccel.stringToKey("CTRL+Q"),
self)
self.connect(self.actions["fileQuit"],
SIGNAL("activated()"),
self.slotFileQuit)
self.actions["editDoc"] = QAction("Edit",
fileQuitIcon,
"&Edit",
QAccel.stringToKey("CTRL+E"),
self)
self.connect(self.actions["editDoc"],
SIGNAL("activated()"),
self.slotEditDoc)
Populating toolbars, menubars and statusbars are always a bit tedious. When
BlackAdder is integrated with Qt 3.0, it will be possible to design not only dialogs
and widgets, but also menus and toolbars using a very comfortable action editor. I
will discuss the various aspects of creating toolbars and menubars later in Chapter
13.
def initMenuBar(self):
self.fileMenu = QPopupMenu()
self.actions["fileQuit"].addTo(self.fileMenu)
self.menuBar().insertItem("&File", self.fileMenu)
self.editMenu = QPopupMenu()
self.actions["editDoc"].addTo(self.editMenu)
self.menuBar().insertItem("&Edit", self.editMenu)
def initToolBar(self):
self.fileToolbar = QToolBar(self, "file operations")
self.actions["fileQuit"].addTo(self.fileToolbar)
QWhatsThis.whatsThisButton(self.fileToolbar)
280
Chapter 12. Application Frameworks
def initStatusBar(self):
self.statusBar().message("Ready...")
def initDoc(self):
self.doc=DocviewDoc()
The view is created after the document, and then made into the central application
widget.
def initView(self):
self.view = DocviewView( self.doc, self)
self.setCentralWidget(self.view)
This function is called in the slotFileQuit() slot when the document has been
modified. Note that were using a class function, information, from
QMessageBox. By passing an empty string after the button labels for "Ok" and
"Cancel", the messagebox is created with only two buttons, instead of three.
def queryExit(self):
exit = QMessageBox.information(self,
"Quit...",
"Do you re-
ally want to quit?",
"&Ok",
"&Cancel",
"", 0, 1)
if exit==0:
return TRUE
else:
return FALSE
The slot functions are called whenever one of the QActions is activated(). Note
how the statusbar message is set, before calling the document functions directly.
281
Chapter 12. Application Frameworks
#
# Slot implementations
#
def slotFileQuit(self):
self.statusBar().message("Exiting application...")
if self.doc.isModified():
if self.queryExit():
qApp.quit()
else:
qApp.quit()
self.statusBar().message("Ready...")
def slotEditDoc(self):
self.doc.slotModify()
def main(args):
app=QApplication(args)
docview = DocviewApp()
app.setMainWidget(docview)
docview.show()
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
This is the stub that starts the application. In contrast with the examples from Part I,
such as hello5.py, this framework doesnt check if all windows are closed with:
app.connect(app, SIGNAL("lastWindowClosed()")
, app, SLOT("quit()"))
This is because the framework supports only one window, and quitting the app is
integrated in the DocviewApp class.
Now the startup bit is done, we can see what docview.py produces when it is run:
282
Chapter 12. Application Frameworks
This framework only supports one window with one view and one document.
Another omission is that there is no interaction between view and document.
Usually, you will also allow the view component to receive user actions, like mouse
clicks. These mostly arrive in the form of events. You can handle these in various
ways. The first is to directly call the relevant slot functions in the document. Try
adding the following method to the DocviewView class:
def initView(self):
self.view = DocviewView( self.doc, self)
self.setCentralWidget(self.view)
self.connect(self.view, PYSIGNAL("sigViewDoubleClick"),
self.slotEditDoc)
283
Chapter 12. Application Frameworks
As you can see, you can either call the document directly from the view, or via the
application controller. The approach you choose depends on the complexity of your
application. In the rest of this part we will extend this simple framework to include
MDI (multiple document interface) and MTI (multiple top-level windows interface)
applications.
#
# Scripting a docview application
#
doc=DocviewDoc()
doc.slotModify()
You can handle your applications data not only through a GUI interface, but also
with scripts. Possible extensions would be to expose the DocviewDoc functionality
through interfaces like CORBA, DCOM, DCOP or SOAP - all very feasible, since
Python modules to create these interfaces are readily available. Note however that
only CORBA and SOAP are platform independent: DCOM ties you to Windows
and DCOP to KDE. Integrating a macro extension in the application is covered in
Chapter 20
284
Chapter 12. Application Frameworks
Here, the datamodel directory contains SQL scripts to create the database. The
directory dbobj contains a module which handles database access. The directory
dialogs contains designer .ui files. The directory html contains the application
documentation in html format - it would be better to write the documentation in
docbook and automatically generate html and pdf versions of the manual. Finally,
the kuraapp, kuralib and kuragui directories are Python modules (that will
have to be put on the Python path) for the application, document and custom
285
Chapter 12. Application Frameworks
widgets needed for this application. notes contains implementation notes and
pixmaps graphics needed for the toolbar. The rest of the files speak for themselves:
starter scripts for Windows and Unix, and various standard files, like INSTALL and
CHANGELOG
In contrast with Java application, it is not wise to nest Python modules keeping
the structure relatively flat makes it easier to import modules into each other.
Generally, Python applications start out very simple, with just one script file, and
then blossom out in modules and directories. The development of the codebase in
this part is a lively demonstration of that fact.
286
Chapter 13. Actions: menus, toolbars
and accelerators
In this chapter we investigate adding the command structure to the application
framework we developed in Chapter 12. This consists of QAction objects that are
added to toolbars and menu bars, and that can provide keyboard accelerators.
Creating QActions and populating your toolbars and menus with them is not an
exciting task, but it is necessary for any GUI application. Lets take a deeper look at
the possibilities of actions, toolbars and menus.
13.1. Actions
We first encountered the QAction class in Chapter 10. To recap briefly, QAction is
a model of a well-defined action that a user can perpetrate against your application,
or the data of your application models. This a very powerful concept. Previously,
you would have to create menu items and connect them to slots, create toolbar items
and connect them to slots, and create keyboard shortcuts and connect them to slots,
too.
Keeping everything synchronized is difficult, and the whole process often leads to a
horrible mess. Combine this with the possibility that you might want to disable
some actionsuch as redo, when theres nothing to redoand youre suddenly
writing lots of duplicated code. And then, of course, you get to the point where your
users want to modify the contents of the toolbar...
By bringing all the user-interface characteristics of actions together in the QAction
class, most of the mess can be avoided. The QAction class tracks the following
interface elements:
Tooltip text
287
Chapter 13. Actions: menus, toolbars and accelerators
Statusbar tips
Keyboard accelerators
Associated icons
Enabled/disabled
Toggled on or off
For each of these properties, there is a set function; although you can also set some
properties in the constructor of QAction. Here is an annotated example of a
complete QAction definition:
planAction=QAction(self)
planAction.setIconSet(QIconSet(QPixmap(plan)))
planAction.setText("Plan")
planAction.setMenuText("&Plan ...")
planAction.setOn(0)
planAction.setStatusTip("Enable the cunning plan")
planAction.setToolTip("Enables the cunning plan")
planAction.setWhatsThis(
"""Plan
288
Chapter 13. Actions: menus, toolbars and accelerators
planAction.setAccel(QAccel.stringToKey("CTRL+C"),)
planAc-
tion.setToggleAction(1) (10)
There are three constructors that create a QAction with some of the properties
already set. However, I find it nicer to create an empty QAction and set
everything by hand. Besides, the next thing we would like to do is to read in the
definition of a QAction from an XML file or a Python dictionary, and
automatically generate the actions. If you want to do this, it is just as easy to set
every property separately.
One important parameter is the parent to the QAction. You can create groups
of QActions QActionGroups. A QActionGroup is a type of QAction that
functions like a type of QGroupBox: it can group actions into mutually
exclusive choices. The whole group of actions can be added to a menu.
The set of icons you associate with an action is used to paint the toolbar buttons.
They are also used to decorate the pull-down menus. QIconSet can generate
icons in different sizes and with an active, normal or disabled look all by itself,
based on one iconor you can explicitly set icons of different sizes. You can
provide a user option to toggle QMainWindow.setUsesBigPixmaps on or off.
This decides whether the toolbars will be drawn with big icons, or small ones.
This is a generic text that can be used where a more specific text hasnt been set.
For instance, if you enable captions in toolbars with
QMainWindow.setUsesTextLabel() , then this text will be used. It will also
be used for pulldown menu texts, unless you set those explicitly with
setMenuText().
By setting the menutext explicitly, you can add keyboard shortcuts (like alt-p in
this case), or the three dots that indicate that a dialog window will open. You
wouldnt want the shortcut to show up on toolbar captionsthey dont work
there, so you can set them in the menu text, by prefixing an ampersand (&) to
the shortcut letter.
289
Chapter 13. Actions: menus, toolbars and accelerators
activated()
toggled(boolean)
By connecting these signals to the correct slots (either directly in the document, or
proxy slots defined in the application interface), you have encapsulated the entire
behavior of your interface.
290
Chapter 13. Actions: menus, toolbars and accelerators
self.connect(planaction,
SIGNAL("activated()"),
self.slotExecuteCunningPlan)
self.connect(planaction,
SIGNAL("toggled(bool)"),
self.slotActivateCunningPlan)
self.planaction.addTo(self.planMenu)
self.planaction.addTo(self.toolBar)
13.2. Menus
Most of the time, you can simply add actions or action groups to menus. This is
what we did above, in the docview framework. However, in some cases it is
desirable to have a dynamic list of menu options. This is a bit more complicated.
This is useful, for instance, in applications where the user can open more than one
window, or for lists of recently opened files. In almost every other case, it is awfully
confusing to the user if menu options are added and removed at the whim of the
application.
By connecting the aboutToShow() signal of a menu to a slot (for example,
slotWindowMenuAboutToShow ), you can exercise precise control over the
contents of the menu by first clearing it, and then building it up again. Note the use
of setItemChecked() to place a checkmark next to selected windows. This is
something you get for free with a QActionGroup, but recreating a QActionGroup
every time the user selects the window menu is just as much a bore as recreating the
menu, if not more so.
def slotWindowMenuAboutToShow(self):
self.windowMenu.clear()
291
Chapter 13. Actions: menus, toolbars and accelerators
self.actions["windowNewWindow"].addTo(self.windowMenu)
self.actions["windowCascade"].addTo(self.windowMenu)
self.actions["windowTile"].addTo(self.windowMenu)
if self.workspace.windowList()==[]:
self.actions["windowAction"].setEnabled(FALSE)
else:
self.actions["windowAction"].setEnabled(TRUE)
self.windowMenu.insertSeparator()
13.3. Toolbars
As you can see, once you have defined your set of QActions, you have little to
worry about concerning the definition of menus and toolbars. However, if you want
other widgets beyond simple buttons in your toolbars, you cannot use QAction or
QActionGroup. One popular addition to a toolbar is a combobox. Adding the
combobox means quite a bit of work:
self.fileToolbar.addSeparator()
self.labelSelector=QLabel("Font: ", self.fileToolbar)
self.comboSelector=QComboBox(self.fileToolbar)
292
Chapter 13. Actions: menus, toolbars and accelerators
self.comboSelector.insertStrList(["Times","Arial","Cyberbit"], 1)
self.comboSelector.setEditable(FALSE)
self.connect(self.comboSelector,
SIGNAL("activated(int)"),
self.slotFontChanged)
QWhatsThis.add(self.comboSelector,"""Font Selection
Select the font that expresses your
personality best.""")
First, we give the widget a bit more room by adding a separator. Then we need a
label, so people will know what the contents of the combobox represent. Of course,
a combobox must be populated with relevant items (font names in this case). We
ensure that people cant add items to the list, nor change the names of existing
items, by setting editable to false. Finally, the activated signal is connected to a
likely slot and a whats this text is added. The result looks like this:
That whats this text leads us to a special button offered by PyQt. This is the little
arrow-plus-question-mark that activates the Whats This mode. When the user
selects this button, he or she can click on any interface element. A yellow note with
a bit of explanation is then shown. This is particularly welcome when your toolbar
icons are non-intuitive.
Achieving this demands two steps: actually setting the help text in the QAction,
and adding the appropriate button to the toolbar:
QWhatsThis.whatsThisButton(self.fileToolbar)
293
Chapter 13. Actions: menus, toolbars and accelerators
If you want to embed a small picture of the icon in the yellow note, you use PyQts
QMimeSourceFactory . The trick is to first set the text to the widget, with an
embedded img link pointing towards an identifier in the text. This identifier will be
used to look up an image in the application-wide default mime source factory.
You can use source factories for every kind of data for which a mime-type exists,
but they are most often used for simple pictures that have to be embedded in rich
text. Rich text in Qt is text which is marked up with a subset of html. The <img>
tags dont point to an URL, but to the contents of the mime source factory.
self.actions["fileQuit"].setWhatsThis(
"""<img source="filequit">Quit<br><br>
By selecting quit you leave the application.
If your document was changed, you will be
asked to save it, so its quite safe to do.""")
QMimeSourceFactory.defaultFactory().setPixmap(filequit,
QPixmap(filequit))
You can add this code to the initActions() method of the DocviewApp in the
document-view framework. Mime source factories are very capable beasts. We can
feed the factory just the QPixmap which is derived from the XPM data in the
resources.
294
Chapter 13. Actions: menus, toolbars and accelerators
doesnt close the application unless the file menu happens to be open. Accelerators
are important and every menu option and every widget on a dialog ought to be
associated with one.
Accelerators, on the other hand, are generally combinations of CTRL and some
other key (or, for the WordPerfect-conditioned octopodi amongst us, all modifiers
together; CTRL-SHIFT-ALT-F12, anyone?). If the focus of your application is on
the document (or on a text field in a dialog box), then your user will want to use
shortcuts for everything.
There are a number of standard accelerators, such as CTRL-X for the cut action, but
every application is free to add to that store. You associate accelerators with
QActions, with the setAccel function. For actions that are not associated with
QActions, you can create QAccel objects on their own, and connect them to a slot
of your choice.
QAccel offers one especially handy class method, stringToKey, which takes a
string describing an accelerator and returns and integer value that represents a key
combination to PyQt. The format is very clear: "Ctrl+O", for instance. Even better,
because this is a string, you can translate this string using pygettext or PyQts tr()
to localize the keyboard shortcuts for different countries. This is not possible if you
use the Qt constants that represent keys, such as Qt.CTRL + Qt.Key_O.
class DocviewApp(QMainWindow):
"""
DocviewApp combines DocviewDoc and DocviewView
into a single window, single document application.
"""
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
self.setIcon(QPixmap(appicon))
295
Chapter 13. Actions: menus, toolbars and accelerators
This is the icon that will be shown in the titlebar and on the taskbar. The application
icon that will be used for shortcuts on the desktop is a different story altogether.
There is absolutely no standard way of setting the icon that is used for a shortcut on
the Unix desktop. If you target Gnome, KDE or any of the other Unix desktop
environments, you will have to delve into .desktop files and other things. Still, it is
possible to create a working .desktop file, for instance for KDE:
[Desktop Entry]
Exec=python main.py
Icon=appicon
Type=Application
DocPath=framework/index.html
MapNotify=true
Name=SDI framework
Name[nl]=SDI raamwerk
This one is for the KDE desktop, and exactly where this should go is a deployment
issue. The same file should, theoretically, also be usable for Gnome.
On the other hand, setting a default icon for Windows applications is quite
impossible, since Windows expects the icon to be part of the application binary.
296
Chapter 14. Automatic testing with
PyUnit
In Chapter 12, we created an application framework in which the GUI interface was
separate from the application logic. One of the reasons for this was to make it easier
to test the components in isolation. Testing your software components separately is
called unit-testing, and it has proven over the past few years to be a very good way
of ensuring software quality. Python supports working with unit-tests out of the
box: since version 2.1, a special module, unittest.py, is included in the standard
distribution. In this chapter, we will write a unittest for the document module from
the document-view framework.
297
Chapter 14. Automatic testing with PyUnit
everything works as intended, and a horrible, unfriendly red progressbar when a test
fails. You can also run the unittests without a gui, but it isnt as much fun.
Back to the drawing board; the bar is red, tests have failed!
Writing tests takes a bit of getting used-to, and it is something more easily learned
when working together with someone who has done it before. However, once you
get around to it, it is definitely addictive.
Unit-testing using the unittest.py framework also departs from what people are
used to doing when testing: namely, writing scripts that simulate user input, such as
mouse-clicks. Those scripting solutions are quite often so fragile that they are worse
than useless. It is far better to explicitly code tests for the back-end of your
application, guaranteeing that the interaction between backend and GUI is correct,
as opposed to trying to deduce bugs from apparent errors at the GUI front.
In sum, the advantage of unit-testing is: you know you can depend upon the
behavior of your components, and whenever you change a component, you will be
298
Chapter 14. Automatic testing with PyUnit
alerted to that change by failing tests. In short, you will be able to trust your
software at a relatively low level.
There a few disadvantages, too. You might be lulled into a false sense of security: if
you change your unit-tests along with the code, then you can no longer be sure that
your components fit your system, for you have just changed their behavior. A
unittest is a kind of contract about the behavior your code exposes to the outside
world. Changing the contract one-sidedly is a guarantee for breaking relations.
Its also quite difficult to find a good division between unit-tests and functional
tests. Functional testing is mostly done from a user perspective; unit-tests test the
behavior of your classes, but functional tests test the behavior of the application.
There is currently no way to automate functional testing.
Cynics have noted that the running of unittests has negated all the progress made in
creating fast compilers, and even virtually compilation-less languages such as
Python. Indeed, running a full testsuite can take a long time. Fortunately, Pyunit is
very fast.
Lastly, watching the bar stay green is addictive in itself, and you might be tempted
to run working tests over and over again...
299
Chapter 14. Automatic testing with PyUnit
Once you have the supporting odds and ends in place, you can start writing tests for
your application.
class SimpleTest(unittest.TestCase):
def runTest(self):
assert 1=2, One is not two
We want to check whether the DocviewDoc class can be instantiated. This is not as
trivial and silly as it may sound: a Python class constructor can have a great and
variable number of arguments, and keeping classes instantiatable is quite important.
First, lets define a testcase:
#
# dvt1.py - a simple test of instantiating a document
#
import unittest
from docviewdoc import DocviewDoc
300
Chapter 14. Automatic testing with PyUnit
class DocviewDocTestCase(unittest.TestCase):
"""DocviewDocTestCase test the DocviewDoc class.
"""
def setUp(self):
print "setUp called"
def tearDown(self):
print "tearDown called"
def runTest(self):
"""Check whether the document could be instantiated"""
doc=None
doc=DocviewDoc()
assert doc!=null, Could not instantiate DocviewDoc
Adding a docstring, always a good idea, is particularly useful for TestCase classes,
since this text will be available through the shortDescription() function. It will
be displayed when the test is run - either on the command line or in the gui.
The setUp method of the TestCase class is executed before runTest, and can be
used to prepare the environment. The tearDown method is called after the test has
run, whether successfully or not. You can use it to clear away garbage that is left by
the test. On the other hand, if setUp fails, the entire test will not run. I have
included the functions here; you dont have to write them if you dont need them.
The test itself is very simple. First, we give the variable doc a known value, namely
None. After that, a DocviewDoc is instantiated. Using Pythons standard assert
statement, a check is made to see whether the variable doc still points to Noneif
so, instantiation failed.
Note that use of assert means that running unit tests with optimized bytecode
(.pyo files) is useless. Compiling Python to optimized bytecode removes all traces
of asserts (and the line-numbers that can be used for stack traces). While the unittest
framework includes a special, home-brew, assert function that isnt affected by
optimized compilation, it is still better to test plain Python. The stacktraces are far
more useful, and additionally, the unittest assert is less convenient.
Pythons assert statement is very handy, and quite simple. It takes the form of:
301
Chapter 14. Automatic testing with PyUnit
Where the message will be printed and an AssertionError raised when the
expression turns out to be false. assert is a statement, not a function. This means
that you shouldnt encase the expression and the message in brackets. If the
statement is too long for the line, you can use a backslash as a line-continuation.
def suite():
testSuite=unittest.TestSuite()
testSuite.addTest(DocviewDocTestCase())
return testSuite
In order to be able to execute this file its handy to add a bit of executable code to
the end:
def main():
runner = unittest.TextTestRunner()
runner.run(suite())
if __name__=="__main__":
main()
302
Chapter 14. Automatic testing with PyUnit
Running the test will give you the satisfaction of knowing that at least one piece of
your application is working:
OK
Or, if you want to see the green and pleasant bar, you can run the gui testrunner.
Enter the name of the testmodule (dvt1 in this case) in the textbox, followed by the
name of the function that returns the testsuite:
Then press run, and sit back. Please note that the actual output you get might differ.
The python unittesting framework is in constant development. For the screen output
in this chapter, I used version 1.3.0, which is included with the other sources that
belong to this book. The unittest gui has been brought up to date to the version of
unittest.py thats included with Python 2.2.
303
Chapter 14. Automatic testing with PyUnit
bit messy and chaotic to write separate testcase classes for those two tests.
Additionally, in many cases you will have to prepare an environment for those tests,
and it would be a pity to duplicate that code across many test classes.
It is therefore possible to create more than one testing method for each testcase. For
each test method a separate instance of the test object is created and added to the
test suite. These methods customarily start with check, but thats not necessary.
#
# dvt2.py - a simple test of instantiating a document
#
import sys
import unittest
from docviewdoc import DocviewDoc
class DocviewDocTestCase(unittest.TestCase):
"""DocviewDocTestCase test the DocviewDoc class.
"""
def checkInstantion(self):
"""Check whether the document could be instantiated"""
doc=None
doc=DocviewDoc()
except:
self.fail("Could not instantiate document for rea-
son: " +
sys.exc_info()[0])
else:
assert doc!=None, Could not instanti-
ate DocviewDoc
def checkModifiable(self):
"""Check whether the document could be modified"""
doc=DocviewDoc()
doc.slotModify()
assert doc.isModified(), Docu-
ment could not be modified
def checkUniverse(self):
"""Check whether the universe is still sane"""
try:
val = 1 / 0
304
Chapter 14. Automatic testing with PyUnit
except ZeroDivisionError:
pass # all natural laws still hold
else:
fail ("The universe has been demolished and re-
placed with chaos.")
def suite():
testSuite=unittest.TestSuite()
testSuite.addTest(DocviewDocTestCase("checkInstantion"))
testSuite.addTest(DocviewDocTestCase("checkModifiable"))
return testSuite
def main():
runner = unittest.TextTestRunner()
runner.run(suite())
if __name__=="__main__":
main()
def checkFailUnless(self):
self.failUnless(1==1, "One should be one.")
def checkFailIf(self):
305
Chapter 14. Automatic testing with PyUnit
The first argument is the exception that should be raised. The second argument of
assertRaises() must be a callable object, such as a function. The other
arguments are simply the arguments that should be passed to the function.
#
# unittests - unittests for the whole system.
#
# dvt1 tests the creation of a docviewdoc
#dvt1.suite
# dvt2 tests a whole lot more
dvt2.suite
306
Chapter 14. Automatic testing with PyUnit
If you use the following script, then all tests that are defined in the form of
module.function, where module is on the Python path and function returns a
TestSuite object, will be combined in one mega-TestSuite.
#
# systemtest.py -
run all tests that are not commented out in unittests
#
import unittest
def suite():
testSuite=unittest.TestSuite()
f=open("unittests")
for t in f.readlines():
t=t.strip() # remove all whitespace
if t[0]!="#": # a comment
testSuite.addTest(unittest.createTestInstance(t))
return testSuite
def main():
runner = unittest.TextTestRunner()
runner.run(suite())
if __name__=="__main__":
main()
#
# dvt3.py - using makeSuite
#
307
Chapter 14. Automatic testing with PyUnit
import sys
import unittest
from docviewdoc import DocviewDoc
class DocviewDocTestCase(unittest.TestCase):
"""DocviewDocTestCase test the DocviewDoc class.
"""
def checkInstantion(self):
"""Check whether the document could be instantiated"""
doc=None
doc=DocviewDoc()
assert doc!=None, Could not instantiate DocviewDoc
def checkModifiable(self):
"""Check whether the document could be modified"""
doc=DocviewDoc()
doc.slotModify()
assert doc.isModified(), Docu-
ment could not be modified
def suite():
testSuite=unittest.makeSuite(DocviewDocTestCase, "check")
return testSuite
def main():
runner = unittest.TextTestRunner()
runner.run(suite())
if __name__=="__main__":
main()
By always prefixing your tests with check, you make sure they are all included. If
you had to add every test by hand, it would be only natural to forget one or two over
time. Eventually you would notice that a test was not being executed. By that time
you might have changed the tested code so the original test fails. The purpose of
unit testing is always to be sure that everything works as you think it should.
308
Chapter 14. Automatic testing with PyUnit
#
# signals.py - unit-testing signals
#
import sys
import unittest
import types
from docviewdoc import DocviewDoc
from qt import *
class ConnectionBox(QObject):
309
Chapter 14. Automatic testing with PyUnit
self.args=[]
class SignalsTestCase(unittest.TestCase):
"""This testcase tests the testing of signals
"""
def setUp(self):
self.doc=DocviewDoc()
self.connectionBox=ConnectionBox()
def tearaDown(self):
self.doc.disConnect()
self.doc=None
310
Chapter 14. Automatic testing with PyUnit
self.connectionBox=None
def checkSignalDoesArrive(self):
"""Check whether the sigDocModified signal arrives"""
self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
self.connectionBox.slotSlot)
self.doc.slotModify()
self.connectionBox.assertSignalArrived("sigDocModified")
def checkSignalDoesNotArrive(self):
"""Check whether the sigDocModifiedXXX sig-
nal does not arrive"""
self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModifiedXXX")
self.connectionBox.slotSlot)
self.doc.slotModify()
try:
self.connectionBox.assertSignalArrived("sigDocModifiedXXX")
except AssertionError:
pass
else:
fail("The signal _did_ arrive")
def checkArgumentToSignal(self):
"""Check whether the sigDocModified sig-
nal has the right
number of arguments
"""
self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
self.connectionBox.slotSlot)
self.doc.slotModify()
self.connectionBox.assertNumberOfArguments(1)
def checkArgumentTypes(self):
"""Check whether the sigDocModified sig-
nal has the right
type of arguments.
"""
self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
self.connectionBox.slotSlot)
self.doc.slotModify()
self.connectionBox.assertArgumentTypes(types.IntType)
311
Chapter 14. Automatic testing with PyUnit
def suite():
testSuite=unittest.makeSuite(SignalsTestCase, "check")
return testSuite
def main():
runner = unittest.TextTestRunner()
runner.run(suite())
if __name__=="__main__":
main()
OK
14.8. Conclusion
I hope I have convinced you that writing unittests is fun, rewarding, more
productive and guaranteed to give you a reputation for infallible code because, in
a measure, that is what you will get for writing tests. As I said in the introduction to
this book, using Python is all about using the best practices. And best in this context
312
Chapter 14. Automatic testing with PyUnit
means productive. If your way of working helps you make less mistakes, the
productivity benefit is enormous. Keep that bar green!
313
Chapter 14. Automatic testing with PyUnit
314
Chapter 15. A More Complex
Framework: Multiple Documents,
Multiple Views
15.1. Introduction
In Chapter 12 we saw a fairly simple framework. The document-view framework
allowed one document and one view on that document. In this chapter we will
explore more complex configurations.
In this chapter, we will go full tilt immediately, with a framework that supports
multiple documents, and with more than one view on those documents. Of course,
this means that you have to keep track of which view shows which document. If a
document changes, all its views must be updated. If the last view on a document is
closed, then the document must be closed. Getting these features right demands
quite complex code.
And this is only the conceptual framework. The actual GUI interface is interesting
too. There are two or three schools in multiple-document interface design: MDI,
MTW, and MTI.
These are cryptic acronyms, but the differences are easy to understand. MDI stands
for Multiple Document Interface. This is an interface in which you have several
windows inside a an application workspace (which is a window on the desktop). It
was invented by Microsoft, who recently tried to bury it. Some users found it rather
confusing. However, other users clamored for its return, and Microsoft appears to
have appeased their wishes, and reinstated the MDI paradigm in full. As found in
most programming environments, in the MDI paradigm there may be both dockable
windows that snap to the sides of the main window, and free-floating windows.
PyQt supports this way of working with the QWorkSpace class. Its a pity that in
PyQt 2 dockable windows are not supported for that you need to use
QDockWindow in PyQt 3.
315
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
An MDI workspace
MTW stands for Multiple Toplevel Window. This is more in favor with the
Unix/X11 crowd. Here, an application litters the entire desktop with various
windows: document windows, floating toolbars, dialog windows everything.
This, too, can be enormously confusing. It works for X11 because most users are
sophisticated and have access to a system with multiple desktops. Early versions of
Visual Basic also used this strategy, and even now, you can select it as an option.
You can create as many QMainWindow objects in PyQt as you wish, so this style is
not a problem at all.
316
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
Finally, there is the style made popular by IDEs such as JBuilder. Here, you have
your documents in one window, but with a row of tabs that you can use to switch
between windows (these tabs are located on the top, bottom or side of the window).
There is a legitimate complaint against this style, too: you cannot compare two
documents side by side. This style is often used by what I refer to as iso-standard
IDEs: Visual Studio like environments, with a project pane, output pane, and at the
top-right corner a stack of tabbed editor windows. BlackAdder conforms, too
except that every project is an MDI-window.
317
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
318
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
saving under a new filename, and closing documents without saving. This is quite
complex enough for now; in the next chapter, we will investigate the addition of
switchable interface styles. After that we will have, at last, a framework that we can
add a real application to.
import unittest
from docmanager import *
from qt import *
class TestViewManager(QObject):
319
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
def activeWindow(self):
return None
def width(self):
return 100
def height(self):
return 100
class TestParent(QObject):
class TestDocument(QObject):
def modified(self):
return TRUE
def save(self):
pass
def close(self):
pass
def title(self):
return "title"
def pathName(self):
return "pathname"
320
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
class TestView(QObject):
321
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
The TestDocument class also shows a clear interface: but more than that, it is also
clearly meant for file-oriented applications. A database application would in all
likelihood not concern itself with obscurities like pathnames. On the other hand,
with a database application it is even more important to allow more than one view
on more than one document at a time if we simply equate document with query.
class DocManagerTestCase(unittest.TestCase):
def setUp(self):
self.parent = TestParent()
self.viewManager = TestViewManager()
def checkInstantiate(self):
try:
docManager = DocMan-
ager(self.parent, self.viewManager)
except Exception, e:
self.fail("Could not instantiate docman-
ager: " + str(e))
def checkCreateDocument(self):
docManager = DocManager(self.parent, self.viewManager)
numberOfDocs = docManager.numberOfDocuments() + 1
numberOfViews = docManager.numberOfViews() + 1
try:
document = docMan-
ager.createDocument(TestDocument, TestView)
except Exception, e:
self.fail("Could not add a new docu-
ment: " + str(e))
def checkAddView(self):
322
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
try:
view = docManager.addView(document, TestView)
except DocManagerError, e:
self.fail(e)
except Exception, e:
self.fail("Could not add a view to a docu-
ment " + str(e))
view = None
document = TestDocument()
try:
view = docManager.addView(document, TestView)
fail("Should not have been able to add a view " +
"to an unmanaged document")
except DocManagerError, e:
pass
assert view == None,\
"View created"
def checkCloseView(self):
docManager = DocManager(self.parent, self.viewManager)
document = docMan-
ager.createDocument(TestDocument, TestView)
323
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
def doNotCheckCloseDocument(self):
docManager = DocManager(self.parent, self.viewManager)
document = docMan-
ager.createDocument(TestDocument, TestView)
docManager.closeDocument(document)
assert docManager.numberOfDocuments() == 0,\
"docManager still manages a document"
def suite():
testSuite=unittest.makeSuite(DocManagerTestCase, "check")
return testSuite
def main():
runner = unittest.TextTestRunner()
runner.run(suite())
if __name__=="__main__":
main()
324
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
"""
docmanager.py -- manager class for document/view mappings
"""
from qt import *
TRUE=1
FALSE=0
class DocManagerError(Exception):pass
class NoSuchDocumentError(DocManagerError):
325
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
class DocumentsRemainingError(DocManagerError):
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
If you have a complex class like the document manager, it is often useful to create a
few specific exception classes. You can still raise exceptions that will be mere
messages in a string but these have been deprecated since Python 2.0. For the
document manager we have a small hierarchy of exceptions, with a base exception
(DocManagerError ), and two specific exceptions, NoSuchDocumentError and
DocumentsRemainingError . The first exception is raised when an attempt is
made to delete a document which is not managed by the document manager. This
can happen when you need more than one document manager, for instance. The
second is raised when an attempt is made to delete all open documents, but one or
more of them could not be closed.
class DocManager(QObject):
"""
The DocManager manages the creation and re-
moval of documents
and views.
"""
def __init__(self, parent, viewManager = None):
QObject.__init__(self)
self._viewToDocMap = {}
326
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
self._docToViewMap = {}
self._parent=parent
if viewManager:
self._viewManager = viewManager
else:
self._viewManager = parent
Two very simple datastructures manage all the information in the document
manage. The first is _viewToDocMap, which maps documents to views (one
document can be associated with a list of views). The other datastructure,
_docToViewMap, maps views to documents. Note the single underscore before the
variable names; this indicates that you shouldnt try to use the variable outside its
class, in this case DocManager. The viewManager is the object that collects all
views and shows them in the application letterbox between toolbars and statusbars.
def numberOfDocuments(self):
return len(self._docToViewMap)
def numberOfViews(self):
return len(self._viewToDocMap)
327
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
return view
328
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
else:
raise DocManagerError(document)
Adding a new view to an existing document is fairly simple: just create the view and
map it to a document, and vice versa. Note that if the document does not exist, we
raise a DocManagerError the document object apparently doesnt belong to
this manager.
if self._docToViewMap.has_key(document):
self._docToViewMap[document].append(view)
else:
self._docToViewMap[document] = [view]
self._viewToDocMap[view] = document
self.emit(PYSIGNAL("sigNumberOfDocsChanged"),())
return view
def activeDocument(self):
if self._viewManager.activeWindow() is not None:
re-
turn self._viewToDocMap[self._viewManager.activeWindow()]
else:
return None
Since the QWorkSpace class, which is the model for the view manager, knows
which window is active, we can use that to determine which document is active.
329
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
QMessageBox.critical(self,
"Error",
"Could not save the cur-
rent document: " + e)
raise e
The things that can go wrong when trying to save a document are manifold
however, we assume that the document knows when to shout "Exception". If that
happens, the user is informed, and the exception re-raised.
330
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
try:
self._docToViewMap[document].remove(view)
del self._viewToDocMap[view]
except ValueError, e:
pass # apparently already deleted
def closeAllDocuments(self):
for document in self._docToViewMap.keys():
if not self.closeDocument(document):
raise DocumentsRemainingError()
Getting rid of documents and views can become quite complicated if you take into
consideration all the various methods available: a user can click on the close button
in the titlebar of the application, or in the view, or activate the "close" QAction. In
order to catch the first possibility, we need to use event filters. Clicking on the close
button does not generate a signal we can connect to. That being so, we should only
call close() on the view, if we know that the closing has not been initiated
through the event filter (otherwise we would fire the event filter again).
331
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
However, when the user selects "close document" or "close all documents" from the
menu or the toolbar, close() will not be automatically called on the view we
have to do this ourselves. By looping through all views in the document, and
closing them, we will generate an event: the event will be handled by the event
filter, which will call closeView() for us. And closeView() will ask the user
whether it really wants to close the document if the view is the last one.
Its an interesting exercise to follow this happening with the BlackAdder debugger.
15.4. Document
As with the simple document/view framework, the document class should know as
little as possible about the actual arrangements. As you can see, little has changed
compared to the simple document-view application of Chapter 12.
"""
mdidoc.py -- document or application model.
"""
from qt import *
from resources import TRUE, FALSE
class MDIDoc(QObject):
"""
The document represents the application model. The current
document keeps a modified state.
"""
def __init__(self, *args):
apply(QObject.__init__, (self,)+args)
332
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
self.newDocument()
self._fileName=None
self._title="Untitled"
def pathName(self):
return self._fileName
def title(self):
return self._title
def newDocument(self):
self.slotModify(FALSE)
def modified(self):
return self._modified
def close(self):
pass
333
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
15.5. View
As with the document, the view is still a relatively uncomplicated object. It still
knows which document it belongs to. I have added close(self) to make it easier
to follow the execution flow when closing a window. What is more important,
however, is the closeEvent() function this is subclassed to completely
override QWidgets default functionality. Close events are handled by the
application itself, not by the view. The view doesnt know anything about the
document manager, which ultimately handles the close event for all views.
"""
mdiview.py -- view component
class MDIView(QWidget):
"""
The MDIView class can represent object of class
MDIDoc on screen.
slots:
slotDocModified
"""
def __init__(self, parent, doc, *args):
334
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
def document(self):
return self.doc
335
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
"""
mdiapp.py -- application class for the mdi framework
from qt import *
class MDIApp(QMainWindow):
"""
MDIApp combines MDIDoc and MDIView into an single
window, multiple sub-
window, multiple document application.
"""
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
self.setCaption("MDI Application Framework")
self.workspace = self.initWorkSpace()
self.docManager=DocManager(self, self.workspace)
self.connect(self.docManager,
PYSIGNAL("sigNumberOfDocsChanged"),
self.setActionsEnabled)
self.initActions()
self.initMenuBar()
self.initToolBar()
self.initStatusBar()
self.setActionsEnabled()
#
# GUI initialization
#
336
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
def initActions(self):
fileNewIcon=QIconSet(QPixmap(filenew))
fileQuitIcon=QIconSet(QPixmap(filequit))
fileOpenIcon=QIconSet(QPixmap(fileopen))
fileSaveIcon=QIconSet(QPixmap(filesave))
self.actions = {}
self.actions["fileNew"] = QAction("New",
fileNewIcon,
"&New",
QAccel.stringToKey("CTRL+N"),
self)
self.connect(self.actions["fileNew"],
SIGNAL("activated()"),
self.slotFileNew)
self.actions["fileOpen"] = QAction("Open",
fileOpenIcon,
"&Open",
QAccel.stringToKey("CTRL+O"),
self)
self.connect(self.actions["fileOpen"],
SIGNAL("activated()"),
self.slotFileOpen)
self.actions["fileSave"] = QAction("Save",
fileSaveIcon,
"&Save",
QAccel.stringToKey(""),
self)
self.connect(self.actions["fileSave"],
SIGNAL("activated()"),
self.slotFileSave)
337
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
self.connect(self.actions["fileSaveAs"],
SIGNAL("activated()"),
self.slotFileSaveAs)
self.actions["fileClose"] = QAction("Close",
"&Close Document",
QAccel.stringToKey("CTRL+W"),
self)
self.connect(self.actions["fileClose"],
SIGNAL("activated()"),
self.slotFileClose)
self.actions["fileQuit"] = QAction("Exit",
fileQuitIcon,
"E&xit",
QAccel.stringToKey("CTRL+Q"),
self)
self.connect(self.actions["fileQuit"],
SIGNAL("activated()"),
self.slotFileQuit)
self.actions["editDoc"] = QAction("Edit",
fileQuitIcon,
"&Edit",
QAccel.stringToKey("CTRL+E"),
self)
self.connect(self.actions["editDoc"],
SIGNAL("activated()"),
self.slotEditDoc)
self.actions["windowCloseWindow"] = QAction(self)
self.actions["windowCloseWindow"].setText("Close Window")
self.actions["windowCloseWindow"].setAccel(QAccel.
stringToKey("CTRL+W"))
self.actions["windowCloseWindow"].setMenuText("&Close Window")
self.connect(self.actions["windowCloseWindow"],
SIGNAL("activated()"),
self.slotWindowCloseWindow)
self.actions["windowNewWindow"] = QAction(self)
self.actions["windowNewWindow"].setText("New Window")
338
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
self.actions["windowNewWindow"].setMenuText("&New Window")
self.connect(self.actions["windowNewWindow"],
SIGNAL("activated()"),
self.slotWindowNewWindow)
self.actions["windowCascade"] = QAction(self)
self.actions["windowCascade"].setText("Cascade")
self.actions["windowCascade"].setMenuText("&Cascade")
self.connect(self.actions["windowCascade"],
SIGNAL("activated()"),
self.workspace.cascade)
self.actions["windowTile"] = QAction(self)
self.actions["windowTile"].setText("Tile")
self.actions["windowTile"].setMenuText("&Tile")
self.connect(self.actions["windowTile"],
SIGNAL("activated()"),
self.workspace.tile)
self.actions["windowAction"] = QAction-
Group(self, None, FALSE)
self.actions["windowAction"].insert(self.actions["windowCloseWindo
self.actions["windowAction"].insert(self.actions["windowNewWindow"
self.actions["windowAction"].insert(self.actions["windowCascade"])
self.actions["windowAction"].insert(self.actions["windowTile"])
self.actions["helpAboutApp"] = QAction(self)
self.actions["helpAboutApp"].setText("About")
self.actions["helpAboutApp"].setMenuText("&About...")
self.connect(self.actions["helpAboutApp"],
SIGNAL("activated()"),
self.slotHelpAbout)
The set of actions included in this framework is not complete, of course. Ideally,
you would want accelerators for switching between views, and a lot of application
specific actions. Well be adding these over the next few chapters.
def initMenuBar(self):
self.fileMenu = QPopupMenu()
339
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
self.actions["fileNew"].addTo(self.fileMenu)
self.actions["fileOpen"].addTo(self.fileMenu)
self.actions["fileSave"].addTo(self.fileMenu)
self.actions["fileSaveAs"].addTo(self.fileMenu)
self.actions["fileClose"].addTo(self.fileMenu)
self.fileMenu.insertSeparator()
self.actions["fileQuit"].addTo(self.fileMenu)
self.menuBar().insertItem("&File", self.fileMenu)
self.editMenu = QPopupMenu()
self.actions["editDoc"].addTo(self.editMenu)
self.menuBar().insertItem("&Edit", self.editMenu)
self.windowMenu = QPopupMenu()
self.windowMenu.setCheckable(TRUE)
self.connect(self.windowMenu,
SIGNAL("aboutToShow()"),
self.slotWindowMenuAboutToShow)
self.menuBar().insertItem("&Window", self.windowMenu)
self.helpMenu = QPopupMenu()
self.actions["helpAboutApp"].addTo(self.helpMenu)
self.menuBar().insertItem("&Help", self.helpMenu)
def initToolBar(self):
self.fileToolbar = QToolBar(self, "file operations")
self.actions["fileNew"].addTo(self.fileToolbar)
self.actions["fileQuit"].addTo(self.fileToolbar)
QWhatsThis.whatsThisButton(self.fileToolbar)
def initStatusBar(self):
self.statusBar().message("Ready...")
We have created menus, toolbars and statusbars so often by now that this is merely
an exercise in cutting and pasting. However, note that we create a Window menu,
but we dont add the actions to that menu. This is because the contents of the
window menu are dynamic. Just before showing the window menu, when the signal
"aboutToShow()" is emitted, we will be building the menu from the list of views
340
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
def initWorkSpace(self):
workspace=QWorkspace(self)
self.setCentralWidget(workspace)
return workspace
For now, the view manager is simply an instance of QWorkSpace, which is a very
simple class that manages widgets as sub-windows to itself. For it to manage
widgets, they should be created with the workspace as parent. QWorkSpace has two
methods: activeWindow(), which returns the widget that currently has focus, and
windowList(), which returns the list of all windows.
Furthermore, there are two slots: cascade() and tile(), that arrange the widgets
managed by the workspace. Lastly, there is one signal you can connect to:
windowActivated() , which is fired whenever a widget is activated i.e. gets
focus.
def setActionsEnabled(self):
enabled = self.docManager.numberOfDocuments()
self.actions["fileSave"].setEnabled(enabled)
self.actions["fileClose"].setEnabled(enabled)
self.actions["editDoc"].setEnabled(enabled)
#
# Slot implementations
#
def slotFileNew(self):
341
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
docu-
ment = self.docManager.createDocument(MDIDoc, MDIView)
def slotFileOpen(self):
fileName = QFileDia-
log.getOpenFileName(None, None, self)
if not fileName.isEmpty():
document=MDIDoc()
document.open(fileName)
view = self.docManager.addDocument(document, MDIView)
view.setFocus()
Opening a file is slightly more complicated; we need to be sure that the user
actually selected a file before a file can be opened. Remember that all Qt classes
return QString objects, not Python string objects. As a result, we have to use
isEmpty() instead of comparing with None.
If the filename is not empty, we create an empty document, ask that document to
open the file, and then add the document to the document manager. Of course, this
complexity can also be removed to the document manager, by adding an
openDocument(self, fileName, documentClass, viewClass) function
to DocManager.
342
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
Saving a document entails some complexity: the document may or may not have a
filename; if not, the user should supply one. Saving could fail for a variety of
reasons. Nothing is so frustrating as losing your data because you simply wanted to
save it. An application should handle save errors very carefully to ensure no data is
lost.
def slotFileClose(self):
doc=self.docManager.activeDocument()
self.docManager.closeDocument(doc)
def slotFileQuit(self):
try:
self.docManager.closeAllDocuments()
except:
return
qApp.quit()
Closing a document and quitting the application are closely related processes. Note
the call to qApp.quit() this is only reached when closing all documents
succeeds.
def slotEditDoc(self):
343
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
doc = self.docManager.activeDocument()
doc.slotModify()
def slotWindowCloseWindow(self):
self.workspace.activeWindow().close()
Closing a single window might mean that the document will be closed, too if it is
the last or only view the document has. By retrieving the active window from the
workspace, and calling the close() function on it, a closeEvent will be
generated. This will be caught by the event filter defined below, which calls the
appropriate functions in the document manager.
def slotWindowNewWindow(self):
doc = self.docManager.activeDocument()
self.docManager.addView(doc, MDIView)
def slotHelpAbout(self):
QMessageBox.about(self,
"About...",
"MDI Framework\n" +
"Inspired by the KDevelop tem-
plates.\n" +
"(c) 2001 by Boudewijn Rempt")
Adding a new window is very simple: retrieve the currently active document, and
ask the document manager to add a view for that document.
def slotWindowMenuAboutToShow(self):
self.windowMenu.clear()
self.actions["windowNewWindow"].addTo(self.windowMenu)
self.actions["windowCascade"].addTo(self.windowMenu)
self.actions["windowTile"].addTo(self.windowMenu)
self.windowMenu.insertSeparator()
self.actions["windowCloseWindow"].addTo(self.windowMenu)
if self.workspace.windowList()==[]:
self.actions["windowAction"].setEnabled(FALSE)
else:
self.actions["windowAction"].setEnabled(TRUE)
344
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
self.windowMenu.insertSeparator()
Here, we dynamically create the window menu just before it is shown. The four
menu optionsnew window, cascade, tile and closeare part of a single
QActionGroup, and can be enabled or disabled together. Of course, the same could
be done with the other actions that are only enabled when there are actually
documents in existence. Note also that we add accelerators by numbering the views
(this will, of course, stop being sensible once we have more than nine open
windows).
#
# Toplevel event filter
#
345
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
except Exception, e:
return TRUE
return QWidget.eventFilter(self, object, event)
Qt events contrast with Qt signals in that they are typically created by user actions,
such as key presses or mouse actions. Signals are mostly emitted by objects on
themselves.
An event filter is an object that receives all events for the object to which it applies.
You can install eventfilters that are created for one object in other objects. In this
case, all views share the same event filter as the application object. An eventfilter
must return either true or falsetrue if the event should not be propagated further,
and false if someone should handle the event.
Here, we check whether the event is of the type QEvent.close if that is so, we
check whether it is meant for the main application window (thats us the self).
In that case, all documents must be closed. This event is generated when the user
closes the application.
If the event is meant for one of the sub-windows, the document manager is asked to
close the view. If that is successful, the event is accept()-ed, and will not be
propagated any further.
#
# Functions called from the document manager
#
def queryCloseDocument(self, document):
r = QMessageBox.information(self,
str(self.caption()),
"Do you want to close %s?" %
document.title(),
"Yes",
"No",
None,
0, 1)
if r == 0:
return QMessageBox.Yes
else:
return QMessageBox.No
346
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
347
Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views
These calls to QMessageBox and the standard file dialog QFileDialog are made
from the document manager. This makes sure that the document manager can also
work without a GUI.
The QMessageBox class is a bit messy, by Qt standards. There are two ways of
specifying buttons: by string, or by identity. These identities, like
QMessageBox.Yes are defined in the class. If you use these constants in your calls
to QMessageBox.warning() , for instance, then the return value will be the
identity of the button pressed.
However, if you want the added flexibility of translatable strings, you cannot use the
identities. You can call functions like QMessageBox.warning() with strings, but
the return value will be the position of the key pressed, starting with 0 and going
from left to right.
I want to use the identities in the document manager this makes the code a lot
clearer. But I wanted to use strings in the actual message boxes. Thats why I
translate the position of the button pressed to the correct identity.
15.7. Conclusion
In this chapter we have laid a secure foundation for a complex multi-document
application. This foundation, with only minor cosmetic changes, can be used over
and again. For file-based MDI applications it is a perfect fit, but the same principles
hold for database applications. In the next chapter, we will explore alternatives for
the MDI paradigm; and when this is done, we are ready to start the real work of
creating an application.
348
Chapter 16. User Interface Paradigms
In Chapter 15, we created a general framework to handle the complexities of
applications that have more than one document open at the same time with
possibly more than one view on the same document, too. We also discussed the
various paradigms for representing those views in the application.
In this chapter, we will explore the actual implementation of some of those
paradigms, starting with one of the most useful and modern paradigms: the tabbed
document model.
349
Chapter 16. User Interface Paradigms
"""
tabmanager.py - tabbed document manager for the mdi framework
class TabManager(QTabWidget):
350
Chapter 16. User Interface Paradigms
Adding a new view is a simple exercise. However, note that until you actually call
showPage() on your view, the QTabWidget appears to be innocent of your
addition, and wont manage the layout of the page. This means that when you create
a new window and resize the application window, the contents wont resize with it.
Simply drawing the tab widgets attention to the page will suffice, however.
With PyQts QWorkspace it was enough to create a widget with the workspace as
its parentthe widget was automatically managed shown. This is no longer enough
when we use QTabWidget. This means that we will have to adapt the DocManager
class to work with addView. This is done in the private _createView() function:
self._viewManager.addView(view)
view.installEventFilter(self._parent)
if self._viewToDocMap == {}:
view.showMaximized()
else:
view.show()
return view
351
Chapter 16. User Interface Paradigms
def activeWindow(self):
return self.currentPage()
def windowList(self):
return self.views
The first of these three functions is new. Simply closing a widget was enough to
remove it when it was managed by the QWorkspace object; now we must explicitly
remove it. This, too, demands a change in the DocManager class, but fortunately,
its a simple change:
Both activeWindow() and windowList have been included to make the interface
of the tabmanager more similar to that of QWorkspace. If you want to have
transparently interchangeable components, they must have the same functions.
def canCascade(self):
return FALSE
def canTile(self):
return FALSE
352
Chapter 16. User Interface Paradigms
You cannot cascade nor tile a set of tab pages. The functions are included, but
merely to avoid runtime exceptions when the application inadvertently does try to
call them. The functions canCascade() and canTile() can be used to determine
whether this component supports this functionality.
"""
workspace.py - MDI workspace class for the mdi framework
class WorkSpace(QWorkspace):
def canCascade(self):
return TRUE
def canTile(self):
return TRUE
353
Chapter 16. User Interface Paradigms
That didnt hurt, did it? We added a mere four functions to the interface for our
view managers. Adding and removing a view from a workspace doesnt need our
active intervention, so we can simply add stubs for addView and removeView.
And a workspace can both cascade and tile, so canCascade() and canTile()
return TRUE.
The other functions we had to define in TabManager, like cascade() or
windowList are part of QWorkspace and we dont need to reimplement them.
"""
splitspace.py - splitter view manager for the mdi framework
354
Chapter 16. User Interface Paradigms
Clever and clean as Qt might be, it is not immune to the inevitable inconsistencies
caused by prolonged development. Some classes, such as the QTabWidget we saw
above, have special insert or add methods for the insertion or addition of child
widgets; others, like QWorkspace take care of their children if those children are
created with them as the parent. This also holds for QSplitter create a widget
with a QSplitter object as a parent, and it will be automatically managed by the
splitter. Therefore the addView() function has little to do.
def activeWindow(self):
for view in self.views:
if view.hasFocus():
return view
return self.views[0]
In order to be able to figure out which of the widgets managed by the splitter is the
currently active one, we have to loop over the list and retrieve the one with focus. If
that fails, we fall back on a hack: just return the first one.
def canCascade(self):
return FALSE
def canTile(self):
return FALSE
355
Chapter 16. User Interface Paradigms
"""
stackspace.py - stacked view manager for the mdi framework
class StackSpace(QWidgetStack):
QWidgetStack is one of those classes that wants its children to be explicitly added
and removed. You also have to give a numerical ID to identify the widget.
def activeWindow(self):
return self.visibleWidget()
356
Chapter 16. User Interface Paradigms
def canCascade(self):
return FALSE
def canTile(self):
return FALSE
def windowList(self):
return self.views
becomes:
357
Chapter 16. User Interface Paradigms
"""
listspace.py -
stacked view manager with a list for the mdi framework
class ListSpace(QSplitter):
The ListSpace is based on QSplitter that way the user can decide how wide
he wants to have his list of window titles.
First the QListBox is added to the splitter, and then to the widget stack (which is
used in the same way as in the previous section). Here, I chose to use a QListBox,
because it offers a more comfortable interface for the adding, changing and
removing of entries than a QListView. As soon as we need the treeview, column
358
Chapter 16. User Interface Paradigms
Of course, adding a view is now slightly more complicated, because the caption of
the view must also be inserted into the listbox. Note that we have changed the code
of MDIView slightly: when its caption changes, it now emits a signal, which we use
here to keep the title text in the listview synchronized with the title of the document.
Synchronization is done using the setListText function, which uses the view to
determine the right entry in the listbox. Of course, the mapping between the view
object and the entry in the listbox should be encapsulated in a subclass of
QListBox.
359
Chapter 16. User Interface Paradigms
def activeWindow(self):
return self.stack.visibleWidget()
def canCascade(self):
return FALSE
def canTile(self):
return FALSE
def windowList(self):
return self.views
16.6. Conclusion
In this chapter, we have created numerous different ways of managing views on
documents from simple stacks, to multiple child windows, to tabbed documents.
What has been missing is the paradigm in which creating a new view or document
requires adding a new top-level window to the desktop. This paradigm doesnt quite
fit into the framework we developed in this chapter, unfortunately.
360
Chapter 16. User Interface Paradigms
Having achieved this tremendous flexibility means nothing to the user if he has to
hack the source code to use it. In Chapter 18, we will investigate retrieving, setting
and saving user options, after weve added some functionality to the application in
the next chapter.
Notes
1. This is one area where the cleverness of PyQt makes life a bit more difficult
than you might like. In C++, you remove a QListViewItem by deleting it.
The parent QListView or QListViewItem then forgets about the child item,
too. However, sip keeps a reference to the QListViewItem; deleting the item
from Python wont make any differenceas long as the parent keeps a
reference to the child, sip will keep one, too. There is a function takeItem(),
but its use is fraught with danger. You might want to try the
item.parent().removeChild(item) trick if you want to remove items
from a QListView.
361
Chapter 16. User Interface Paradigms
362
Chapter 17. Creating Application
Functionality
17.1. Introduction
In the last few chapters, we have built a useful framework for a file-oriented
application. In this chapter we add some real functionality to the document and
view modules.
As an example application, I propose to develop a reasonably full-featured editor,
using the standard PyQt multi-line editor widget. This will give us a chance to
explore some outlying regions of the Qt library without having to handle the
complexity of a custom created widget (as we would have to do if we were to
create, for example, a paint application). Not that PyQt isnt capable of this, or of
nice games you can make those, too, if you want. Later, we will have occasion to
look at the versatile canvas widget, which has been used for many things, including
web browsers, games, and the eric debugger that is part of PyQt.
We will extend our project with an input method for Unicode text, which will give
us a chance to work with QCanvas, search and replace, and macros. Additionally,
there are some fun items that we can add, such as a rolling chart that keeps track of
how fast you type.
363
Chapter 17. Creating Application Functionality
"""
kalamview.py - the editor view component for Kalam
class KalamMultiLineEdit(QMultiLineEdit):
364
Chapter 17. Creating Application Functionality
QMultiLineEdit.keyPressEvent(self, e)
return TRUE
else:
return QMultiLineEdit.event(self, e)
By default the QWidgets event() function filters out all tab (and shift-tab)
presses. Those keys are used for focus management, and move the focus to the next
widget. This is not what we want in an editor, where pressing tab should insert a
TAB character in the text. By overriding the default event() function, we can
correct this behavior. If the typeand there are more than seventy event types in
PyQtis QEvent.KeyPress , we send the event directly to the keyPressEvent
method, instead of moving focus. In all other cases, we let our parent class,
QMultiLineEdit handle the event.
class KalamView(QWidget):
"""
The KalamView class can represent object of class
KalamDoc on screen, using a standard edit control.
signals:
sigCaptionChanged
"""
def __init__(self, parent, doc, *args):
apply(QWidget.__init__,(self, parent) + args)
self.layout=QHBoxLayout(self)
self.editor=KalamMultiLineEdit(self)
self.layout.addWidget(self.editor)
self.doc = doc
self.editor.setText(self.doc.text())
self.connect(self.doc,
PYSIGNAL("sigDocTitleChanged"),
self.setCaption)
self.connect(self.doc,
PYSIGNAL("sigDocTextChanged"),
self.setText)
self.connect(self.editor,
365
Chapter 17. Creating Application Functionality
SIGNAL("textChanged()"),
self.changeDocument)
self._propagateChanges = TRUE
The basic view is a plain QWidget that contains a layout manager (QHBoxLayout)
that manages a KalamMultiLineEdit widget. By strictly wrapping the
KalamMultiLineEdit functionality, instead of inheriting and extending, it will be
easier to swap this relatively underpowered component for something with a bit
more oomph and espieglerie, such as QTextEdit or KDEs editor component,
libkwrite. Or, perhaps, a home-grown editor component we wrote in Python...
In the framework, we set the background color initially to green; the same principle
holds here, only now we set the text initially to the text of the document.
The first two connections speak for themselves: if the title of the document changes,
the caption of the window should change; and if the text of the document changes
(perhaps through editing in another view), our text should change, too.
The last connection is a bit more interesting. Since we are wrapping a
QMultiLineEdit in the KalamView widget, we have to pass changes in the editor
to the outside world. The textChanged() signal is fired whenever the user
changes the text in a QMultiLineEdit widget (for instance, by pasting a string or
by typing characters).
When you use functions that are not defined as slots in C++ to change the text
programmatically, textChanged() is not emitted. We will wrap these functions
and make them emit signals, too.
def document(self):
return self.doc
366
Chapter 17. Creating Application Functionality
def changeDocument(self):
if self._propagateChanges:
self.doc.setText(self.editor.text(), self)
367
Chapter 17. Creating Application Functionality
if self != view:
self.disconnect(self.editor,
SIGNAL("textChanged()"),
self.changeDocument)
self.editor.setText(text)
self.connect(self.editor,
SIGNAL("textChanged()"),
self.changeDocument)
Note that changing the text of a QMultiLineEdit does not change the cursor
position in the editor. This makes life a lot easier, because otherwise we would have
to move the cursor back to the original position ourselves in all dependent views.
After all, the purpose of having multiple views on the same document is to enable
the user to have more than one cursor location at the same time.
368
Chapter 17. Creating Application Functionality
mangle strings as Python does, so we dont lose much functionality. (But look
carefully at the documentation for QString some functions, such as
stripWhiteSpace() , return a new string instead of working on the existing
string.)
Would our editor have to store more complex information, instead of plain text, we
should use the KalamDocument class to store its data perhaps in a list of
paragraphs, where each paragraph is a list itself, containing words, lines and
perhaps more complex objects, such as images or even active widgets so you
could embed a button or a hyperlink in your text. However, never code what you
dont yet need is a excellent motto...
"""
kalamdoc.py -
abstraction of a document with a certain encoding
class KalamDoc(QObject):
"""
The document represents a plain text with a certain encod-
ing. Default
is Unicode.
"""
def __init__(self, *args):
apply(QObject.__init__, (self,)+args)
self.encoding="unicode"
self.newDocument()
self._fileName = None
self._title = "Untitled"
self._modified = FALSE
self._text = QString()
369
Chapter 17. Creating Application Functionality
Instead of wrapping a simple, silly boolean value, we now wrap s single QString
object.
Most of the above functions havent changed the basic framework. Note that the
slotModified function has disappeared. Modifying a text isnt as simple as
flipping a single boolean.
The setText function, which is called from the KalamView class, applies brute
force to the text that KalamDocument manages. Quite simply, setText replaces
the internal reference to QString with a reference to the QString that was
presented by the calling view. The text has been modified, and this is recorded in the
administrative _modified variable. Finally, by emitting the
"sigDocTextChanged" Python signal, all views that show this document are told
to update their display.
The view parameter has a default value of None this means the change does not
originate with any view, and will be applied to all views.
370
Chapter 17. Creating Application Functionality
17.4.1. Loading
Loading first:
This is the Qt way of doing things: first, a QFile object is created. If a file with the
name fileName already exists, a QTextStream is used to read the text from the
file. This text is read into a QString object, which is passed on to setText, which
we saw above. If the file doesnt exist, an exception is raised, which is caught in the
application class, KalamApp.
The Pythonic method is a lot shorter:
The net result is the same: the document receives a text in QString format, and all
views are updated. There is no appreciable difference in performance between these
two methods, but if you plan to translate the Python application to C++ at some
time, it might be preferable to work with as many Qt classes as possible.
17.4.2. Saving
Saving text is slightly more critical than loading: what you cant load, you cant
mangle and lose, but if the application refuses to save a text, a user can lose a lot of
371
Chapter 17. Creating Application Functionality
work. Still, there is little you can do when the disk is full, beyond preventing the
application from crashing. As long as Kalam is running, users can still select, copy
and paste text - a lesson I learned with early versions of Word. Note that saving
using QTextStream is not currently possible. QTextStream uses C++ operator
overloading (i.e. <<) to write to a stream, which is not yet available in Python.
if self.pathName() == None:
raise IOError("Could not save docu-
ment: no filename.")
if isinstance(self.pathName(), QString):
self.setPathName(str(self.pathName()))
s=str(self.text())
f = open(self.pathName(), "w")
f.write(s)
if s[-1:] != "\n":
f.write("\n")
f.flush()
self._modified = FALSE
There are a few necessary checks to perform. The first is to make sure that the
document actually possesses a filename; then we check whether the filename is an
instance of QString, instead of a Python string. Pythons file object cannot use
QStrings it needs to have a genuine Python string. So, if the pathname is an
instance of QString, it is converted to a Python string.
The document text is then converted to a Python string. A Python file object is
created by using the open function, and we write the string to it. If the last character
is not a newline, we write a last newline and flush the file. It is a good idea to end all
files with a newline, though you may wish to make this is a user-option in the
application.
372
Chapter 17. Creating Application Functionality
def clear(self):
self.editor.clear()
def deselect(self):
self.editor.deselect()
def selectAll(self):
self.editor.selectAll()
def paste(self):
self.editor.paste()
def copy(self):
self.editor.copy()
def cut(self):
self.editor.cut()
373
Chapter 17. Creating Application Functionality
def undo(self):
self.editor.undo()
def redo(self):
self.editor.redo()
Of course, this initially looks very silly, and we could just as well directly call the
QMultiLineEdit editor object variable but by encapsulating the editor
component we are free to substitute another component without having to hack the
other components of the application.
The other changes are in the KalamApp class. First, a set of QActions is added to
the dictionary of actions.
Some of these actions have an associated toolbar or menubar icon defined. The icon
data is defined in the resources.py file. Ive used the GPLed toolbar icons from
the KDE project. It is always a good idea to blend in as closely to the desktop
environment you are targetting, so you might also want to provide a set of Windows
standard icons and make it a configuration option which set should be used.
I do not show the full code, just the bits that are new compared to the previous
chapter:
def initActions(self):
self.actions = {}
...
#
# Edit actions
#
self.actions["editClear"] = QAction("Clear",
"C&lear",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editClear"],
SIGNAL("activated()"),
self.slotEditClear)
374
Chapter 17. Creating Application Functionality
self.actions["editSelectAll"] = QAction("SelectAll",
"&SelectAll",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editSelectAll"],
SIGNAL("activated()"),
self.slotEditSelectAll)
self.actions["editDeselect"] = QAction("Deselect",
"Clear selection",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editDeselect"],
SIGNAL("activated()"),
self.slotEditDeselect)
self.actions["editCut"] = QAction("Cut",
QIconSet(QPixmap(editcut)),
"C&ut",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editCut"],
SIGNAL("activated()"),
self.slotEditCut)
self.actions["editCopy"] = QAction("Copy",
QIconSet(QPixmap(editcopy)),
"&Copy",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editCopy"],
SIGNAL("activated()"),
self.slotEditCopy)
self.actions["editPaste"] = QAction("Paste",
QIconSet(QPixmap(editpaste)),
"&Paste",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editPaste"],
375
Chapter 17. Creating Application Functionality
SIGNAL("activated()"),
self.slotEditPaste)
self.actions["editInsert"] = QAction("Insert",
"&Insert",
QAccel.stringToKey(""),
self)
self.connect(self.actions["editInsert"],
SIGNAL("activated()"),
self.slotEditInsert)
self.actions["editUndo"] = QAction("Undo",
QIconSet(QPixmap(editundo)),
"&Undo",
QAccel.stringToKey("CTRL+Z"),
self)
self.connect(self.actions["editUndo"],
SIGNAL("activated()"),
self.slotEditUndo)
self.actions["editRedo"] = QAction("Redo",
QIconSet(QPixmap(editredo)),
"&Redo",
QAccel.stringToKey("CTRL+R"),
self)
self.connect(self.actions["editRedo"],
SIGNAL("activated()"),
self.slotEditRedo)
As you can see, there is still a fair amount of drudgery involved in creating a GUI
interface. Qt 3.0 provides an extended GUI Designer that lets you design actions,
menubars and toolbars with a comfortable interface.
For now, well have to distribute the actions by hand in the initMenu() and
initToolbar() functions. Again, omitted code is elided with three dots (...).
def initMenuBar(self):
...
376
Chapter 17. Creating Application Functionality
self.editMenu = QPopupMenu()
self.actions["editUndo"].addTo(self.editMenu)
self.actions["editRedo"].addTo(self.editMenu)
self.editMenu.insertSeparator()
self.actions["editCut"].addTo(self.editMenu)
self.actions["editCopy"].addTo(self.editMenu)
self.actions["editPaste"].addTo(self.editMenu)
self.actions["editSelectAll"].addTo(self.editMenu)
self.actions["editDeselect"].addTo(self.editMenu)
self.actions["editClear"].addTo(self.editMenu)
self.menuBar().insertItem("&Edit", self.editMenu)
...
def initToolBar(self):
...
...
Finally, we have to define the actual slots called by the QAction objects. Note that
we are not working directly on the document if we did, then all actions (such as
selecting text) would apply to all views of the document. We would also have to
code an undo-redo stack ourselves. Instead, we retrieve the active view from the
workspace manager, and work on that. This view will pass the command on to the
QMultiLineEdit object, and propagate all changes to the relevant document.
# Edit slots
def slotEditClear(self):
self.workspace.activeWindow().clear()
def slotEditDeselect(self):
377
Chapter 17. Creating Application Functionality
self.workspace.activeWindow().deselect()
def slotEditSelectAll(self):
self.workspace.activeWindow().selectAll()
def slotEditPaste(self):
self.workspace.activeWindow().paste()
def slotEditCopy(self):
self.workspace.activeWindow().copy()
def slotEditCut(self):
self.workspace.activeWindow().cut()
def slotEditInsert(self):
self.workspace.activeWindow().insert()
def slotEditUndo(self):
self.workspace.activeWindow().undo()
def slotEditRedo(self):
self.workspace.activeWindow().redo()
17.6. Conclusion
We now have a fairly capable editor on our hands. It can load and save documents,
handle more than one document at a time, handle more than one view on a
document, and can potentially use any of a number of interface paradigms.
However, our editor is still not a Unicode editor, and as such, you will have
problems if you enter a single euro sign, let alone a bit of Cyrillic. You cannot
search and replace, and you cannot set the font. In fact, our editor is not at all
configurable. We will remedy these omissions in the next few chapters.
378
Chapter 18. Application Configuration
Every user has a preferred way of doing things and a good application should be
accommodating enough that important choicessuch as what font to use can be
set by the user. Of course, no one likes applying their favorite settings every time
they start an application, so we will need to store the settings, too. This chapter
deals with settings retrieving, storing and using.
Unfortunately, different platforms have vastly differing traditions for the storing of
user preferences. You can either use the platform standard, or create your own
solution.
379
Chapter 18. Application Configuration
os.environ["HOME"]
This returns None when HOME is not set. If you are developing for KDE, you
might want to store the user settings in $HOME/.kde/share/config/ instead, and
the application settings in $KDEDIR/share/apps .
As the name implies, _winreg is a very low-level library, and only suitable to build
something on top that is more complete. Furthermore, the way in which
ConfigParser deals with settings, while very elegant, is not really compatible
with _winreg. We will first take a look at the easy way out: after all, within the
380
Chapter 18. Application Configuration
foreseeable future well have Qt 3.0s QConfig, which will obsolete our own
efforts.
If you want to keep your application firmly in the Python domainperhaps with a
view to later translate the application to another GUI toolkityou can use
ConfigParser and _winreg (for Windows and Unix, respectively). You can
determine which platform your application runs on with the following check:
if sys.platform=="win32":
import _winreg
# handle reading and writing of configuration data using
# the registry
else:
import ConfigParser
# handle reading and writing of configuration data using
# the configuration files that are structured like windows
# .ini files.
Discussing these standard Python library modules is a bit beyond the scope of this
book. You can find descriptions of them in the Python Library Reference.
Regardless of the solution you choose, you should be able to use the same central
configuration object an object which we are now going to develop.
381
Chapter 18. Application Configuration
available everywhere in the application, because all objects must be able to query
and store settings at will.
In other languages, such as Visual Basic, you would use a module with global
variables to store configuration data; in a language like Java or C++, you would use
a singleton objectthat is, an object with a hidden constructor that can only be
instantiated once. Python, however, does not support these constructions.
Of course, there is an alternative. In a sense, class definitions are global. Every
module that imports a certain class gets exactly the same class. Keep in mind that a
class is just an object, of the type class. You can associate variables not only with
an object, as in:
class SomeClass:
def __init__(self):
self.someVariable=1
someInstance=SomeClass()
print someInstance.someVariable
class SomeClass:
classVariable=1
print SomeClass.classVariable
These class variables are accessed via the name of the class, instead of the name of
an instance of that class. Class variables are shared by all instances of a class.
The ideal solution to creating a "global" configuration repository is to define a class
that contains all configuration data as class variables. Its also possible to
encapsulate the configuration data repository in a single class variable. You cannot
call functions on a class - there is no equivalent to the static methods of Java. If we
need functions to work on the configuration data, we must either define those
functions at module level, or as functions of an object that is a class variable of the
382
Chapter 18. Application Configuration
"""
kalamconfig.py -
Configuration class for the Kalam Unicode Editor
import sys, os
from qt import *
class Config:
APPNAME = "kalam"
APPVERSION = "ch13"
CONFIGFILE = ".kalam-ch13"
currentStyle="Platinum"
viewmanager="tabmanager"
app_x=0
app_y=0
app_w=640
app_h=420
fontfamily="courier"
pointsize=12
weight=50
italic=0
encoding=22
def getApplicationFont():
return QFont(Config.fontfamily,
383
Chapter 18. Application Configuration
Config.pointsize,
Config.weight,
Config.italic,
Config.encoding )
As you can see, its just a simple matter of a class with a bunch of class variables
that represent pertinent values. However, because these values will be saved to a
file, you cannot associate real objects with the keys. To make it easier to retrieve a
font based on the values stored in the configuration file, there is module-level helper
function, getApplicationFont() , which constructs a QFont on the fly.
A similar function exists to set the font:
def setApplicationFont(qfont):
Config.fontfamily = qfont.family()
Config.pointsize = qfont.pointSize()
Config.weight = qfont.weight()
Config.italic = qfont.italic()
Config.encoding = qfont.encoding()
As you can see, we store our settings in a flat namespace, in which every key must
be unique. This is just like the properties system used in Java, but more complex
systems can be very useful. For instance, the Windows registry is one gigantic tree,
and even the files created by ConfigParser have sections and subsections. For
highly complex configuration needs, there is the shlex Python module, which you
can use to define configuration languages.
What we are going to, however, is far more simple. When we load the settings, we
just read every line in the configuration file, and add a variable to the Config class
that represents the value:
384
Chapter 18. Application Configuration
To add the variable to the Config we use the standard Python function setattr()
this function is one of the delights that make Python so dynamic.
Note the special treatment of the value that is represented by "None" in the
configuration file: if "None" is encountered the value of the configuration key is set
to a real None object. This contrast with the situation where the value is simply
empty: then the value is set to an empty string ("").
Currently, the configuration file format only supports two types: strings and
integers. The distinction is made by brute force: we simply try to convert the value
to an integer, and if we succeed, it stays an integer. If the conversion raises a
ValueError, we assume the value should remain a string.
By now you might be wondering when we will be reading in the configuration
values. The simple answer is that we will do so when the KalamConfig module is
first imported. At the bottom of the module the function readConfig(Config) is
called, and is only executed once:
readConfig()
385
Chapter 18. Application Configuration
Saving the configuration values to disk is a simple matter of looping over the
contents of the attributes of the Config class that is, the __dict__,
__methods__ and __members__ dictionaries that are part of the objects hidden
attributes. We retrieve these with the dir() function:
The actual values are retrieved with the opposite of setattr(): getattr(). As a
first check, attributes with a double underscore as prefix are not saved: those are
internal attributes to the Config class. If the value is the None object, we print the
string "None". Because it is quite possible that some values are QString objects,
and because you cannot save these, everything is converted to a plain Python string.
Finally, you might need functions that get and set more complex objects in the
Config. These can be simple module level functions that work on the class:
def getTextFont():
return QFont(Config.fontfamily,
Config.pointsize,
Config.weight,
Config.italic,
Config.encoding )
def setTextFont(qfont):
Config.fontfamily = qfont.family()
Config.pointsize = qfont.pointSize()
Config.weight = qfont.weight()
Config.italic = qfont.italic()
Config.encoding = qfont.encoding()
386
Chapter 18. Application Configuration
"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE
class KalamView(QWidget):
We import the configuration module, not the Config class from the configuration
module. After creating the editor widget, we simply set the font with a call to
self.editor.setFont(kalamconfig.getTextFont()).
387
Chapter 18. Application Configuration
from qt import *
...
import kalamconfig
...
class KalamApp(QMainWindow):
"""KalamApp is the toplevel application win-
dow of the kalam unicode editor
application.
"""
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
...
self.initSettings()
...
#
# GUI initialization
#
def initSettings(self):
qApp.setStyle(kalamconfig.getStyle())
self.setGeometry(kalamconfig.Config.app_x,
kalamconfig.Config.app_y,
kalamconfig.Config.app_w,
kalamconfig.Config.app_h)
388
Chapter 18. Application Configuration
def __extractStyle(style):
if type(style) == InstanceType:
return style.__class__.__name__
elif type(style) == StringType:
return style
else:
return "QPlatinumStyle"
389
Chapter 18. Application Configuration
def setStyle(style):
if type(style) == types.StringType:
Config.currentStyle = style
elif type(style) == types.InstanceType:
Config.currentStyle = __extractStyle(style)
Setting the style in the context of kalamconfig means setting the "currentStyle"
attribute of Config to a string that represents the style. If the input to setStyle()
is already a string (that is, if the type is types.StringType ), then we simply set
it. Otherwise, we use the function defined above to get a string that equals the name
of the style class.
def getStyle():
# Basic sanity check -
you dont want to eval arbitrary code
if not hasattr(Config, "currentStyle"):
print "ok", repr(qApp.style())
Config.currentStyle = __extractStyle(qApp.style())
if (Config.currentStyle[0] != "Q" or
Config.currentStyle[-5:] != "Style" or
Config.currentStyle.find(" ") > 0):
Config.currentStyle = "QPlatinumStyle"
try:
# you shouldnt use eval for this, but it is a nice opportunity
# for showing how it works. Normally youd use a dic-
tionary of
# style names.
return eval(Config.currentStyle)()
except NameError, e:
print "No such style: defaulting to Platinum"
return QPlatinumStyle()
Getting a QStyle object of the right type is a bit more complex. Of course, you will
most often use a simple dictionary that maps style names to style classes:
390
Chapter 18. Application Configuration
This is not particularly flexible. Here, we use eval to create an object from the
name of a class. Look carefully at:
return eval(Config.currentStyle)()
"""
kalamconfig.py -
Configuration class for the Kalam Unicode Editor
391
Chapter 18. Application Configuration
import tabman-
ager, listspace, splitspace, stackspace, workspace
workspacesDictionary = {
"tabmanager" : tabmanager.TabManager,
"listspace" : listspace.ListSpace,
"splitspace" : splitspace.SplitSpace,
"stackspace" : stackspace.StackSpace,
"workspace" : workspace.WorkSpace,
}
class Config:
...
def getViewManager():
try:
return workspacesDictionary[Config.viewmanager]
except:
return tabmanager.TabManager
def setViewManager(viewmanager):
Config.viewmanager = viewmanager.__class__.__name__
These two functions get and set the viewmanager style. If the style given in Config
doesnt exist, a KeyError will be raised, in which case we simply return a sensible
default.
The getViewManager() is called from the initWorkSpace() function in
kalamapp.py:
...
def initWorkSpace(self):
workspace = kalamconfig.getViewManager()(self)
self.setCentralWidget(workspace)
return workspace
392
Chapter 18. Application Configuration
...
...
#
# Slot implementations
#
def slotFileQuit(self):
try:
self.docManager.closeAllDocuments()
except:
return
kalamconfig.writeConfig()
qApp.quit()
...
#
# Toplevel event filter
#
...
def eventFilter(self, object, event):
if (event.type() == QEvent.Close):
if (object<>self):
if self.docManager.closeView(object):
event.accept()
else:
event.ignore()
else:
try:
self.docManager.closeAllDocuments()
393
Chapter 18. Application Configuration
kalamconfig.writeConfig()
event.accept()
except Exception, e:
event.ignore()
return QWidget.eventFilter(self, object, event)
...
394
Chapter 18. Application Configuration
using QSettings. You can either read all settings from the registry or configuration
file and assign them as attributes to the Config object, or you can open a
QSettings object and add that to Config; then you can query the settings object
directly every time you need a value.
The first approach has a few advantages: it is compatible with current code, you can
approach settings with a simple Config.geometry.app_x variable, and if the
user removes the configuration file, your app can merrily continue.
The second approach, which is advised by Trolltech, also has advantages. It is
simpler to write, does not demand much startup time, and does not fill the memory
with data that is not (yet) needed. Furthermore, the app can dynamically react to
changes in the configuration file.
I chose the first approach, as it fits better with Kalam. Besides, it gives me the
chance to show the interesting bits and bobs of QSettings without touring Kalam
again.
"""
kalamconfig.py -
Configuration class for the Kalam Unicode Editor
class Config:
defaults = {
"APPNAME" : "kalam",
"APPVERSION" : "ch13",
"viewmanager" : "tabmanager",
"app_x" : 0,
"app_y" : 0,
"app_w" : 640,
"app_h" : 420},
"fontfamily" : "courier",
"pointsize" : 12,
395
Chapter 18. Application Configuration
"weight" : 50,
"italic" : 0,
"encoding" : 22
}
def init(self):
Config.settings = QSettings()
Config.settings(QSettings.Windows,
"/kalam")
Config.settings(QSettings.Unix,
"/usr/local/share/kalam")
...
As you can see, there is an initialization phase (init()), that creates a settings
objects. This is the one place where QSettings is not platform independent, and
you have to add a search path.
Windows and Unix use different search paths. The Windows search path refers to
the registry key under "/Software", and Qt looks through the main branches in the
following order and whenever it encounters a duplicate setting, it takes the last
one read.: HKEY_CURRENT_USER, HKEY_CURRENT_USER,
HKEY_LOCAL_MACHINE, HKEY_LOCAL_MACHINE.
On Unix, the sequence is comprised of the path you added yourself, then
$QTDIR/etc, and then $HOME/.qt. Thats also the directory where all Qt
396
Chapter 18. Application Configuration
applications will save their configuration files. Your application settings are thus
saved in a file that is named from the combination of the first key you save under
("kalam" in this case), and the suffix rc. And that file is places in the .qt directory
in the users home directory.
The class QSettings also offers functions to retrieve strings, lists, doubles,
integers and boolean values from the configuration database, as well as functions to
add, remove and list keys and subkeys.
18.5. Conclusion
To conclude this chapter, I want to show you the result of our labors: the
configuration file $HOME/.kalam-ch13 :
APPNAME=kalam
APPVERSION=ch13
CONFIGFILE=.kalam-ch13
app_h=420
app_w=640
app_x=0
app_y=0
currentStyle=QWindowsStyle
encoding=22
fontfamily=courier
italic=0
pointsize=12
viewmanager=tabmanager
weight=50
By simply editing the values in this file, you can control the appearance of Kalam to
a good extent - power at your fingertips!
397
Chapter 18. Application Configuration
398
Chapter 19. Using Dialog Windows
In this chapter we add a few dialog windows to the Kalam program. Dialog
windows come in two basic flavors: modal and non-modal. Modal dialog windows
block the interface of you application. Settings dialog, file dialogs and such are
typically modal. Non-modal dialogs can stay open while the user continues working
in the application. Search and replace or style dialogs are typical examples of
non-modal dialogs.
I like to show a sample of what the user selects in the dialog. In this tab, the user
can select font, text and background color for the editor windows. These changes
399
Chapter 19. Using Dialog Windows
are reflected in the little label with the "Lorem ipsum" text. There are two more
options: a combobox for selecting the wrapping mode (either no wrapping, wrap to
the maximum line width, or wrap to the width of the window), and a spin box to set
the maximum line width.
Most users will not immediately get what we mean with "Window view" - in the
interface tab w show an example of what we mean, too. I propose to make the
"Look and Feel" selection automatically active, so that doesnt need a preview.
To fill in the preview Ive snapshotted Kalam in all its variations and scaled the
pictures down a lot. Adding these pictures as inline-image data to the dialog would
make loading very slow, since Python is not so quick in reading bytecode files. It is
better to create a pixmaps directory and store the pictures there.
As for the document, we need two settings: the first is the document encoding.
400
Chapter 19. Using Dialog Windows
While Kalam is meant to be a Unicode editor for standard utf8 files, people might
prefer things like iso-8859-1. This is mere window-dressingactually loading and
saving in encodings other than utf-8 will not be implemented for now. The second
option is about document endings. A text file should end with a newline character,
and we added code to make sure it does in Chapter 17ultimately, this should be a
configuration option.
Of course, during the course of development we will expand the contents of these
pages, adding items when we need them. Someone once remarked that a
configuration dialog presents the history of design decisions that were avoided
during developmentand it often feels that way indeed.
You can either call this generated dialog directly from KalamApp, or you can
subclass it and add some intelligence. Since intelligence is what is needed to
synchronize the switches between interface paradigm, we will go ahead and add
subclass the design and add some.
"""
dlgsettings.py - Settings dialog for Kalam.
See: frmsettings.ui
401
Chapter 19. Using Dialog Windows
class DlgSettings(FrmSettings):
def __init__(self,
parent = None,
name = None,
modal = 0,
fl = 0):
FrmSettings.__init__(self, parent, name, modal, fl)
self.textFont = kalamconfig.get("textfont")
self.textBackgroundColor = kalamconfig.get("textbackground")
self.textForegroundColor = kalamconfig.get("textforeground")
self.MDIBackgroundColor = kalamconfig.get("mdibackground")
self.initEditorTab()
self.initInterfaceTab()
self.initDocumentTab()
402
Chapter 19. Using Dialog Windows
# kalamconfig.py
# Get and set - set emits a signal via Config.notifier
#
customGetSetDictionary = {
"style" : (getStyle, setStyle),
"workspace" : (getWorkspace, setWorkspace),
"textfont" : (getTextFont, setTextFont),
"textforeground" : (getTextForeground-
Color, setTextForegroundColor),
"textbackground" : (getTextBackground-
Color, setTextBackgroundColor),
"mdibackground" : (getMDIBackground-
Color, setMDIBackgroundColor),
}
def get(attribute):
if customGetSetDictionary.has_key(attribute):
value = apply(customGetSetDictionary[attribute][0])
else:
value = getattr(Config, attribute)
return value
But, let us continue with dlgsettings.py. There are three tab pages, and every
tab pages has its own initialization function.
def initEditorTab(self):
self.txtEditorPreview.setFont(self.textFont)
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Base, self.textBackgroundColor)
pl.setColor(QColorGroup.Text, self.textForegroundColor)
403
Chapter 19. Using Dialog Windows
self.cmbLineWrapping.setCurrentItem(kalamconfig.get("wrapmode"))
self.spinLineWidth.setValue(kalamconfig.get("linewidth"))
self.connect(self.bnBackgroundColor,
SIGNAL("clicked()"),
self.slotBackgroundColor)
self.connect(self.bnForegroundColor,
SIGNAL("clicked()"),
self.slotForegroundColor)
self.connect(self.bnFont,
SIGNAL("clicked()"),
self.slotFont)
The editor tab shows a nice preview of the font and color combination the user has
chosen. Setting these colors, however, is not as straightforward as you might think.
Qt widget colors are governed by a complex system based around palettes. A palette
(QPalette) contains three color groups (QColorGroup), one that is used if the
widget is active, one that is used if the widget is disabled, and one that is used if the
widget is inactive.
A QColorGroup in its turn, is a set of colors with certain roles:
404
Chapter 19. Using Dialog Windows
All colors are normally calculated from the Background color. Setting the
background color of the editor with the convenience function
405
Chapter 19. Using Dialog Windows
setBackgroundColor() wont have an effect; we must use the Base color in the
relevant QColorGroup.
This system is certainly quite complex, but it allows for tremendous flexibility.
Using it isnt too arduous. First, we retrieve the palette from the editor widget:
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Base, self.textBackgroundColor)
pl.setColor(QColorGroup.Text, self.textForegroundColor)
Then we can use the function setColor, which takes a colorgroup role and a
QColor as arguments. Note that if we use these functions to change the colors of a
widget after it has been shown for the first time, we must call repaint(TRUE) to
force the widget to redraw itself. Otherwise Qts highly optimized drawing engine
becomes confused. This will be done in the slot function thats connected to the
clicked() signal of the color choice buttons.
def initInterfaceTab(self):
self.initStylesCombo()
self.initWindowViewCombo()
self.lblBackgroundColor.setBackgroundColor(self.MDIBackgroundColor
self.connect(self.bnWorkspaceBackgroundColor,
SIGNAL("clicked()"),
self.slotWorkspaceBackgroundColor)
def initDocumentTab(self):
self.initEncodingCombo()
self.chkAddNewLine.setChecked(kalamconfig.get("forcenewline"))
This must be the least complex tab, but no doubt we will be adding to it during the
course of our development of Kalam.
def initStylesCombo(self):
406
Chapter 19. Using Dialog Windows
self.cmbStyle.clear()
styles = kalamconfig.stylesDictionary.keys()
styles.sort()
try:
currentIn-
dex = styles.index(kalamconfig.Config.style)
except:
currentIndex = 0
kalamconfig.setStyle(styles[0])
self.cmbStyle.insertStrList(styles)
self.cmbStyle.setCurrentItem(currentIndex)
self.connect(self.cmbStyle,
SIGNAL("activated(const QString &)"),
self.setStyle)
To make life a lot easer, we have defined a dictionary that maps user-understandable
style names to QStyle classes in kalamconfig. Note that we need, in order to find
out which one is the current style, not the result of kalamconfig.get("style") ,
since that returns a QStyle object, but the actual string in the Config.style
variable.
The keys of this dictionary are used to fill the style combo. Python dictionaries are
unordered, and to ensure that the same style is alwas at the same place in the
combobox, we have to sort the list of keys. Sorting a list is done in place in Python,
and that means that calling sort() on a list doesnt return a list. If wed written:
styles = kalamconfig.stylesDictionary.keys().sort()
407
Chapter 19. Using Dialog Windows
instead, styles would have been set to None... Activating an entry in the styles
combobox emits a signal that is routed to the setStyle() function:
def initWindowViewCombo(self):
self.cmbWindowView.clear()
workspaces = kalamconfig.workspacesDictionary.keys()
workspaces.sort()
try:
currentIn-
dex = workspaces.index(kalamconfig.Config.workspace)
except:
currentIndex = 0
kalamconfig.setWorkspace(workspaces[0])
self.cmbWindowView.insertStrList(workspaces)
self.cmbWindowView.setCurrentItem(currentIndex)
self.connect(self.cmbWindowView,
SIGNAL("activated(const QString &)"),
self.setWorkspacePreview)
408
Chapter 19. Using Dialog Windows
pixmap = QPixmap(os.path.join("./pixmaps",
workspace))
self.pxViewSample.setPixmap(pixmap)
As you can see, application development is messy, and I dont want to hide all the
mess from you. Later, when we make the application distributable in Chapter 26,
we will have to come back to this function and devise a way to make Kalam retrieve
its pictures from the installation directory.
def initEncodingCombo(self):
self.cmbEncoding.clear()
encodings = kalamconfig.codecsDictionary.keys()
encodings.sort()
try:
currentIn-
dex = encodings.index(kalamconfig.get("encoding"))
except:
currentIndex = 0
Config.encoding = encodings[0]
self.cmbEncoding.insertStrList(encodings)
self.cmbEncoding.setCurrentItem(currentIndex)
The list of encodings is defined in kalamconfig, just like the list of styles and
interface types:
codecsDictionary = {
"Unicode" : "utf8",
"Ascii": "ascii",
"West Europe (iso 8859-1)": "iso-8859-1",
"East Europe (iso 8859-2)": "iso-8859-2",
"South Europe (iso 8859-3)": "iso-8859-3",
"North Europe (iso 8859-4)": "iso-8859-4",
"Cyrilic (iso 8859-5)": "iso-8859-5",
"Arabic (iso 8859-6)": "iso-8859-6",
"Greek (iso 8859-7)": "iso-8859-7",
"Hebrew (iso 8859-8)": "iso-8859-8",
409
Chapter 19. Using Dialog Windows
A QMultiLineEdit widget always used Unicode internally, but these codecs are
used as a default setting for loading and saving files. Users load an ascii file, edit it
in Unicode, and save it back to ascii. Theoretically, you can retrieve the users
preferences from his locale. The operating system defines the preferred encoding,
but people seldom work with one encoding, and Kalam is meant to provide users
with a choice.
While the selection of codecs in Python is large, not all important encodings are
available from Python. Japanese (jis, shift-jis, euc-jp), Chinese (gbk) and Tamil
(tscii) are only available in Qt (QTextCodec classes), and not in Python. Codecs for
the tiscii encoding used for Devagari are not available anywhere. You can download
separate Japanese codecs for Python from
http://pseudo.grad.sccs.chukyo-u.ac.jp/~kajiyama/python/. (euc-jp, shift_jis,
iso-2022-jp)
Note also that iso-8859-8 is visually ordered, and you need Qt 3.0 with the
QHebrewCodec to translate iso-8859-8 correctly to Unicode.
def slotForegroundColor(self):
color = QColorDialog.getColor(self.textForegroundColor)
if color.isValid():
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Text, color)
self.textForegroundColor = color
self.txtEditorPreview.repaint(1)
def slotBackgroundColor(self):
color = QColorDialog.getColor(self.textBackgroundColor)
if color.isValid():
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Base, color)
410
Chapter 19. Using Dialog Windows
self.textBackgroundColor = color
self.txtEditorPreview.repaint(1)
def slotWorkspaceBackgroundColor(self):
color = QColorDialog.getColor(self.MDIBackgroundColor)
if color.isValid():
self.MDIBackgroundColor = color
self.lblBackgroundColor.setBackgroundColor(color)
Each of the color selection buttons is connected to one of these color slot functions.
Note that QFontDialog, in contrast with QColorDialog, returns a tuple
consisting of a QFont and a value that indicates whether the user pressed OK or
Cancel. QColorDialog only returns a color; if the color is invalid, then the user
pressed Cancel. This can be confusing, especially since an invalid QColor is just
black. Note that we have to call repaint(1), here, to make sure the editor preview
is updated.
def slotFont(self):
(font, ok) = QFontDialog.getFont(kalamconfig.getTextFont(),
self)
if ok:
self.txtEditorPreview.setFont(font)
self.textFont = font
The QFontDialog does return a tupleand if ok is true, then we update the font of
the preview and also set the textFont variable to reflect the users choice.
Finally, theres a bit of code appended to DlgSettings, to make it possible to run
the dialog on its own (to test all functionality):
if __name__ == __main__:
a = QApplication(sys.argv)
QObject.connect(a,SIGNAL(lastWindowClosed()),a,SLOT(quit()))
w = DlgSettings()
a.setMainWidget(w)
w.show()
a.exec_loop()
411
Chapter 19. Using Dialog Windows
# kalamapp.py
def initActions(self):
self.actions = {}
...
#
# Settings actions
#
self.actions["settingsSettings"] = QAction("Settings",
"&Settings",
QAccel.stringToKey(""),
self)
self.connect(self.actions["settingsSettings"],
SIGNAL("activated()"),
self.slotSettingsSettings)
...
def initMenuBar(self):
...
self.settingsMenu = QPopupMenu()
self.actions["settingsSettings"].addTo(self.settingsMenu)
self.menuBar().insertItem("&Settings", self.settingsMenu)
...
# Settings slots
def slotSettingsSettings(self):
dlg = DlgSettings(self,
"Settings",
TRUE,
Qt.WStyle_Dialog)
412
Chapter 19. Using Dialog Windows
413
Chapter 19. Using Dialog Windows
414
Chapter 19. Using Dialog Windows
dlg.exec_loop()
if dlg.result() == QDialog.Accepted:
kalamconfig.set("textfont", dlg.textFont)
kalamcon-
fig.set("workspace", str(dlg.cmbWindowView.currentText()))
kalamcon-
fig.set("style", str(dlg.cmbStyle.currentText()))
kalamcon-
fig.set("textbackground", dlg.textBackgroundColor)
kalamcon-
fig.set("textforeground", dlg.textForegroundColor)
kalamcon-
fig.set("mdibackground", dlg.MDIBackgroundColor)
kalamcon-
fig.set("wrapmode", dlg.cmbLineWrapping.currentItem())
kalamcon-
fig.set("linewidth", int(str(dlg.spinLineWidth.text())))
kalamcon-
fig.set("encoding", str(dlg.cmbEncoding.currentText()))
kalamcon-
fig.set("forcenewline", dlg.chkAddNewLine.isChecked())
If the execution loop of a modal dialog terminates, the dialog object is not
destroyed, and you can use the reference to the object to retrieve the contents of its
widgets. By calling result() on the dialog object you can determine whether the
user pressed OK or Cancel.
In this example, if the user presses OK, all relevant settings in kalamconfig are
updated. This causes kalamconfig to emit change signals that are caught by all
relevant objects.
The workspace object is updated:
def initWorkSpace(self):
workspace = kalamconfig.get("workspace")(self)
workspace.setBackgroundColor(kalamconfig.get("mdibackground"))
self.connect(qApp,
415
Chapter 19. Using Dialog Windows
PYSIGNAL("sigmdibackgroundChanged"),
workspace.setBackgroundColor)
self.setCentralWidget(workspace)
return workspace
All view objects are updated, too. Some of the changes can be directly connected to
the editor widget, the font setting, while others need a bit of processing, like the
wrap mode:
# kalamview.py - extract
...
import kalamconfig
...
class KalamView(QWidget):
416
Chapter 19. Using Dialog Windows
pl = self.editor.palette()
pl.setColor(QColorGroup.Text, qcolor)
self.editor.repaint(TRUE)
if wrapmode == 0:
self.editor.setWordWrap(QMultiLineEdit.NoWrap)
elif wrapmode == 1:
self.editor.setWordWrap(QMultiLineEdit.WidgetWidth)
else:
self.editor.setWordWrap(QMultiLineEdit.FixedColumnWidth)
self.editor.setWrapColumnOrWidth(kalamconfig.get("linewidth"))
...
Not all changes can be activated while the application is running. The workspace
style is determined when the application is restarted. It is nice and courteous to
inform the user so. The best place to do that is in slotSettingsSettings() :
def slotSettingsSettings(self):
...
if dlg.result() == QDialog.Accepted:
...
workspace = str(dlg.cmbWindowView.currentText())
if kalamconfig.Config.workspace <> workspace:
kalamconfig.set("workspace", workspace)
QMessageBox.information(self,
"Kalam",
"Changes to the inter-
face style " +
"will only be acti-
vated when you " +
"restart the application.")
...
417
Chapter 19. Using Dialog Windows
19.2.1. Design
What we are aiming for is a combined "search" and "search and replace" dialog
box. It should conform to the following requirements:
418
Chapter 19. Using Dialog Windows
...
from dlgfindreplace import DlgFindReplace
...
class KalamApp(QMainWindow):
There are two actions defined: one for search, and one for find and replace. Again,
the "find" icon is the standard KDE 2 icon for find operations.
def initActions(self):
self.actions = {}
...
#
# Edit actions
#
419
Chapter 19. Using Dialog Windows
...
self.actions["editFind"] = QAction("Find",
QIconSet(QPixmap(editfind)),
"&Find",
QAccel.stringToKey("CTRL+F"),
self)
self.connect(self.actions["editFind"],
SIGNAL("activated()"),
self.slotEditFind)
self.actions["editReplace"] = QAction("Replace",
"&Replace",
QAccel.stringToKey("CTRL+R"),
self)
self.connect(self.actions["editReplace"],
SIGNAL("activated()"),
self.slotEditReplace)
By now, you probably know what comes next: adding the actions to the menu bar,
and to the toolbar. Since there isnt an icon for replace, it cannot be added to the
toolbar:
def initMenuBar(self):
...
self.editMenu = QPopupMenu()
...
self.editMenu.insertSeparator()
self.actions["editFind"].addTo(self.editMenu)
self.actions["editReplace"].addTo(self.editMenu)
self.menuBar().insertItem("&Edit", self.editMenu)
...
def initToolBar(self):
...
self.editToolbar = QToolBar(self, "edit operations")
...
self.actions["editFind"].addTo(self.editToolbar)
420
Chapter 19. Using Dialog Windows
Because the combined find/find and replace dialog has two modes, it is necessary to
have two ways of calling itone for find, and one for find and replace. The dialog
should work on the current document and the current view, but it is difficult to
determine if current should be the current document and view when the dialog is
opened, as opposed to the document and view that have focus. The user might, after
all, change document and view while the find dialog is open, or even close them.
For now, lets use the document and view that are open when the dialog is shown.
def slotEditFind(self):
self.dlgFindReplace.showFind(self.docManager.activeDocument(),
self.workspace.activeWindow())
def slotEditReplace(self):
self.dlgFindReplace.showReplace(self.docManager.activeDocument(),
self.workspace.activeWindow())
421
Chapter 19. Using Dialog Windows
self.initOptions(document, view)
"""
dlgfindreplace.py - Findreplace dialog for Kalam.
See: frmfindreplace.ui
422
Chapter 19. Using Dialog Windows
from qt import *
import kalamconfig
class DlgFindReplace(FrmFindReplace):
""" A full-featured search and replace dialog.
"""
def __init__(self,
parent = None,
name = None):
FrmFindReplace.__init__(self, par-
ent, name, FALSE, Qt.WStyle_Dialog)
self.connect(self.bnFind,
SIGNAL("clicked()"),
self.slotFindNext)
self.connect(self.bnReplaceNext,
SIGNAL("clicked()"),
self.slotReplaceNext)
self.connect(self.bnReplaceAll,
SIGNAL("clicked()"),
self.slotReplaceAll)
self.connect(self.radioRegexp,
SIGNAL("clicked()"),
self.slotRegExp)
self.connect(self.chkCaseSensitive,
SIGNAL("clicked()"),
self.slotCaseSensitive)
self.connect(self.chkWholeText,
SIGNAL("clicked()"),
self.slotBeginning)
self.connect(self.chkSelection,
SIGNAL("clicked()"),
self.slotSelection)
self.connect(self.radioForward,
SIGNAL("clicked()"),
self.slotForward)
423
Chapter 19. Using Dialog Windows
self.connect(self.radioBackward,
SIGNAL("clicked()"),
self.slotBackward)
In the constructor we connect all relevant clicked() signals to their slots. The rest
of the initialization (such as determining which text or part of text we will work on)
is moved to the show() function. The same instance of the dialog can be used for
different documents.
424
Chapter 19. Using Dialog Windows
if view.hasSelection():
self.chkSelection.setChecked(TRUE)
self.setFindExtent()
The init() function sets the document and view variables. Most of the work is
done directly on the view, making use of its functionality for inserting, deleting and
selecting text. This is because whenever a string is found, it will be selected. Asking
the document to select a string will cause it to select the string in all views of that
document, which would be quite confusing for the user.
If there is already a selection present in the view, the "find in selection" checkbox is
checked. This is convenient, because when a user presses find after having selected
a section of text, he most likely wants the search to be performed in that selection
only.
The function setFindExtent() (which we will examine in detail later in this
section) determines which part of the text should be searched: from the cursor
position to the end, to the beginning, or between the beginning and end of a
selection. The find routine keeps track of where it is within a search extent, using
the variable self.currentPosition , which is initially the same as the start
position of the extent.
#
# Slot implementations
#
def slotRegExp(self):
if self.radioRegexp.isChecked():
self.radioForward.setChecked(TRUE)
self.grpDirection.setEnabled(FALSE)
else:
self.grpDirection.setEnabled(TRUE)
If you are using Qt 2.3, you cannot use regular expressions to search backwards. In
Qt 3.0 the regular expression object QRegExp has been greatly extended, and can be
used to search both forwards and backwards. When Kalam was written, Qt 3.0 was
still in beta. Therefore, it was necessary to include code to disable the
425
Chapter 19. Using Dialog Windows
c - the character c
426
Chapter 19. Using Dialog Windows
\n - matches newline (10). For instance "else\n" will find all occurrence of
else that are followed with a new line, and that are thus missing the
obligatory closing colon (:).
\r - matches return (13)
def slotCaseSensitive(self):
pass
def slotBeginning(self):
self.setFindExtent()
def slotSelection(self):
self.setFindExtent()
def slotForward(self):
self.setFindExtent()
def slotBackward(self):
427
Chapter 19. Using Dialog Windows
self.setFindExtent()
Whenever the user alters one of the options that influence the direction or area of
search, the extent must be adapted.
#
# Extent calculations
#
def setSelectionExtent(self):
self.startSelection = self.view.selectionStart()
self.endSelection = self.view.selectionEnd()
self.startPosition = self.startSelection
self.endPosition = self.endSelection
def setBackwardExtent(self):
# Determine extent to be searched
if (self.chkWholeText.isChecked()):
self.endPosition = self.view.length()
else:
self.endPosition = self.view.getCursorPosition()
self.startPosition = 0
if self.chkSelection.isChecked():
if self.view.hasSelection():
setSelectionExtent()
self.currentPosition = self.endPosition
def setForwardExtent(self):
# Determine extent to be searched
if (self.chkWholeText.isChecked()):
self.startPosition = 0
else:
self.startPosition = self.view.getCursorPosition()
self.endPosition = self.view.length()
if self.chkSelection.isChecked():
if self.view.hasSelection():
428
Chapter 19. Using Dialog Windows
setSelectionExtent()
self.currentPosition = self.startPosition
def setFindExtent(self):
if self.radioForward.isChecked():
self.setForwardExtent()
else:
self.setBackwardExtent()
Correctly determining which part of the text should be searched is a fairly complex
task. First, there is an important difference between searching forwards and
backwards, if only because of the place where searching should start. A further
complication is caused by the option to search either the whole text, or from the
cursor position. Note that begin and end mean the same thing with both backwards
and forwards searches; it is currentPosition , where searching should start, that
is different between forward and backward searches.
def wrapExtentForward(self):
if QMessageBox.information(self.parent(),
"Kalam",
"End reached. Start from beginning?",
"yes",
"no",
None,
0,
1) == 0:
self.endPosition = self.startPosition
self.startPosition = 0
self.currentPosition = 0
self.slotFindNext()
def wrapExtentBackward(self):
if QMessageBox.information(self.parent(),
"Kalam",
"Be-
gin reached. Start from end?",
"yes",
"no",
429
Chapter 19. Using Dialog Windows
None,
0,
1) == 0:
self.startPosition = self.endPosition
self.endPosition = self.view.length()
self.currentPosition = self.startPosition
self.previousOccurrence()
self.slotFindNext()
Whenever the current extent has been searched, the user should be asked whether he
or she wants to search the rest of the text. The functions above are different for
forwards and backwards searching, too.
#
# Find functions
#
def nextOccurrence(self):
findText = self.cmbFind.currentText()
caseSensitive = self.chkCaseSensitive.isChecked()
if self.radioRegexp.isChecked():
# Note differences with Qt 3.0
regExp = QRegExp(findText,
caseSensitive)
pos, len = regExp.match(self.view.text(),
self.currentPosition,
FALSE)
return pos, pos+len
else:
pos = self.view.text().find(findText,
self.currentPosition,
caseSensitive)
return (pos, pos + findText.length())
Searching forwards can be done by plain text matching, or with regular expressions.
Regular expressions are available from both Python and PyQt. Python regular
expressions (in the re module) work on Python strings, while PyQt regular
expressions work on QStrings. It is relatively inefficient to convert a QString to a
430
Chapter 19. Using Dialog Windows
def previousOccurrence(self):
findText = self.cmbFind.currentText()
caseSensitive = self.chkCaseSensitive.isChecked()
pos = self.view.text().findRev(findText,
self.currentPosition,
caseSensitive)
return (pos, pos + findText.length())
Qt 2.3 doesnt yet support backwards searching with regular expressions, so the
function previousOccurrence is quite a bit simpler. Instead of
QString.find(), QString.findRev() is used - this searches backwards.
def slotFindNext(self):
if self.radioForward.isChecked():
begin, end = self.nextOccurrence()
if begin > -1:
431
Chapter 19. Using Dialog Windows
self.view.setSelection(begin,
end)
self.currentPosition = end
return (begin, end)
else:
if (self.chkSelection.isChecked() == FALSE and
self.chkWholeText.isChecked() == FALSE):
self.wrapExtentForward()
re-
turn (self.currentPosition, self.currentPosition)
else:
begin, end = self.previousOccurrence()
if begin > -1:
self.view.setSelection(begin,
end)
self.currentPosition = begin -1
return (begin, end)
else:
if (self.chkSelection.isChecked() == FALSE and
self.chkWholeText.isChecked() == FALSE):
self.wrapExtentBackward()
re-
turn (self.currentPosition, self.currentPosition)
The slotFindNext slot is the central bit of intelligence in this class. Depending
upon the selected direction, the next or previous occurrence of the search string is
searched. If an occurrence is found (when begin is greater than -1), it is selected,
and the current position is moved. If there are no more matches, the user is asked
whether he or she wants to go on with the rest of the document.
def slotReplaceNext(self):
begin, end = self.slotFindNext()
if self.view.hasSelection():
self.view.replaceSelection(self.cmbReplace.currentText())
return begin, end
else:
return -1, -1
def slotReplaceAll(self):
begin, end = self.slotReplaceNext()
432
Chapter 19. Using Dialog Windows
Replacing is one part finding, one part replacing. The slotFindNext() code is
reused, which is one good reason for creating a dialog that has both a find and a find
and replace mode. slotFindNext() already selects the match, so replacing is a
simple matter of deleting the match and inserting the replacement string. This is
done with a new function in KalamView:
...
class KalamView(QWidget):
...
def replaceSelection(self, text):
self.editor.deleteChar()
self.editor.insert(text)
self.editor.emit(SIGNAL("textChanged()"),())
Messing about with the text in a QMultiLineEdit widget has a few tricky points.
You should avoid trying to directly change the QString that you retrieve with
QMultiLineEdit .text() changing this string behind the editors back is a sure
recipe for a beautiful crash. QMultiLineEdit has several functions, such as
deleteChar() (which not only deletes characters, but also the selection, if there is
one), to alter the contents. However, these functions dont emit the
textChanged() signal you will have to do that yourself. If we do not emit
textChanged(), other views on the same document wont know of the changes,
nor will the document itself know it has been changed.
Another interesting complication occurs because QMultiLineEdit, the editor
widget used in KalamView, works with line and column positioning, not with a
position within the string that represents the text. This makes it necessary to create
conversion functions between string index and editor line / column position in
KalamView, which is potentially very costly business for long files:
...
class KalamView(QWidget):
...
def getLineCol(self, p):
433
Chapter 19. Using Dialog Windows
i=p
for line in range(self.editor.numLines()):
if i < self.editor.textLine(line).length():
return (line, i)
else:
# + 1 to compensate for \n
i-=(self.editor.textLine(line).length() + 1)
# fallback
return (0,0)
def getCursorPosition(self):
"""Get the position of the cursor in the text"""
if self.editor.atBeginning():
return 0
if self.editor.atEnd():
return self.editor.text().length()
l, c = self.editor.getCursorPosition()
return self.getPosition(l, c)
434
Chapter 19. Using Dialog Windows
temporary variable, until the length of the current line is greater than the remaining
number of characters. Then we have linenumber and column.
The same, but in reverse is necessary in getPosition to find out how far into a
string the a certain line number and column position is. There are a few safeguards
and optimizations, but not quite enough.
Qt 3 offers the QTextEdit class, which is vastly more powerful than
QMultiLineEdit . For instance, QTextEdit sports a built-in find function.
Internally, QTextEdit is associated with QTextDocument, which is comparable to
our KalamDocument. But you cant get at QTextDocument (its not even
documented, you need to read the Qt source code to find out about it), so its not a
complete replacement for our document-view architecture. The external rich text
representation of QTextEdit is a subset of html, which makes it less suitable for a
programmers editor. You have to choose: either colorized, fontified text, and filter
the html codes out yourself, or a plain text editor. Fortunately, Qt 3 still includes
QMultiLineEdit for compatibility reasons.
19.3. Conclusion
In this chapter we have created two really complex dialog windows, one that makes
the rest of the application inaccessible, and one that works in tandem with the
application. We have also investigated dynamically constructed signals, color
groups, widget flags and regular expressions. In the next chapter we will add
another level of functionality to Kalam: macros.
435
Chapter 19. Using Dialog Windows
436
Chapter 20. A Macro Language for
Kalam
One thing that separates a run-of-the-mill application from a real toolone that
users will take refuge in day after dayis a good macro facility. Python, which was
designed with ease of use in mind, is a natural choice for a macro language. Nine
out of ten secretaries would choose Python over the WordPerfect macro language or
Visual Basic, given the choice! Isnt it fortunate that we have already begun
developing our application in Python?
This chapter deals with integrating a Python based macro facility in Kalam. In the
course of this chapter we investigate the execution of Python code while
dynamically adding actions and menu items. We also cover granting user access to a
predefined API of our application objects.
Of course, the underlying mechanics of a macro facility are not particular to any
GUI toolkit. And if you decide to convert your application to C++, you can still
embed Python, wrap the API using sip, and allow your users to execute the same
macros. We handle powerful stuff in this chapter, and its well worth the effort.
437
Chapter 20. A Macro Language for Kalam
ginger: this is what your Python interpreter uses to execute your code.
The mere ability to execute random bits of code is quite powerful in itself, but code
only becomes truly useful if it no longer exists in a void, but can call other,
pre-existing bits of code.
The code we execute with eval(), exec() and execfile() should be brought
into relation with the other Python modules that exist in the library, and with the
code of our application. Not only that, but preferably also with the state, that is, the
variables and objects, of the application that asks eval(), exec() and
execfile() to execute the code.
To that end, eval(), exec() and execfile() take two parameters. The first,
globals, is a dictionary that represents the global namespace. You can retrieve the
global namespace of your application with the function globals(). The global
namespace contains all imported classes, built-in functions and all global variables,
but you can also construct a restricted global environment dictionary yourself.
The second argument, locals, is a dictionary that represents the local namespace.
You can retrieve it with locals(). The locals dictionary contains whatever
names are local to the function your application is currently in. You can also create
a restricted (or expanded) locals dictionary yourself.
Warning
If you mess about with the globals and locals dictionary, be
prepared to encounter what the Python Language Reference
calls "undefined behavior". For instance, if you execute a bit of
code with an empty locals dictionary, you cannot add new
names to the namespace. This means that import wont work,
for instance, or even variable assignments. Generally
speaking, it is best to simply pass the globals dictionary, which
means that the locals dictionary used by the executed code
will be a copy of the globals dictionary.
Lets compare these locals and globals from an interactive Python session:
438
Chapter 20. A Macro Language for Kalam
First, we take a look at the contents of the globals dictionary when Python is first
started. Then, we define a simple function f, that creates a variable a, which
contains the value 1 and which prints the locals dictionary. Retrieving the value of
globals shows that f is now part of globals. Running f shows that a is the only
member of locals.
By default, the globals and locals arguments of eval(), exec() and
execfile() contain the current contents of globals and locals, but you can
alter this for instance, to restrict access to certain application objects.
439
Chapter 20. A Macro Language for Kalam
First, we take a look at what "eval" is for a beast. A built-in function. OK, lets try it
out. Yes, 1 equals 1 evaluates to 1, which means TRUE - eval neatly returns the
result of the code it executes. Next, having imported the string module, we use it
to split a string. Here, eval() has access to the global namespace, which means it
can access the module we just imported, so string.split() evaluates just fine.
However, if we try to evaluate the same expression, but with empty global and local
dictionaries, we get a NameError exception - suddenly string isnt known
anymore. Trying to evaluate something more complicated, something that is not a
single expression that returns a value (even if its only None) doesnt work at all -
which is why exec() exists.
440
Chapter 20. A Macro Language for Kalam
441
Chapter 20. A Macro Language for Kalam
First, we create a string that contains the bit of Python we want to execute. Note
how it imports the qt module, and how it uses the string module. Executing the
code doesnt work: it throws a NameError because string isnt known. Importing
string into the global namespace makes it also available to exec, of course.
Executing the code string now succeeds, and a quick peek in globals learns us
that the module qt has been added.
442
Chapter 20. A Macro Language for Kalam
In the middle of all the qt classes the main module of Kalam imports into
globals, we find the main module itself, which isnt there if we just execfile
main.py.
# kalamapp.py
...
class KalamApp(QMainWindow):
...
# Macro slots
...
def slotMacroExecuteDocument(self):
if self.docManager.activeDocument() == None:
QMessageBox.critical(self,
"Kalam",
"No document to exe-
cute as a macro ")
return
try:
byte-
code = compile(str(self.docManager.activeDocument().text()),
"<string>",
"exec")
443
Chapter 20. A Macro Language for Kalam
except Exception, e:
QMessageBox.critical(self,
"Kalam",
"Could not compile " +
self.docManager.activeDocument().title()
"\n" + str(e))
try:
exec bytecode # Note: we dont yet have a sepa-
rate namespace
# for macro execution
except Exception, e:
QMessageBox.critical(self,
"Kalam",
"Error executing " +
self.docManager.activeDocument().title()
"\n" + str(e))
...
We are being a bit careful here, and thus compile the code first to check for syntax
errors. These, along with execution errors, will be shown in a dialog box. Note that
anything you print from here will go to standard outputthat is, a black hole if you
run Kalam by activating an icon, or the terminal window if you run Kalam from the
shell prompt. It would be a logical step to redirect any output to a fresh Kalam
document (this is what Emacs does). It is quite easy to achieve. You can reassign the
standard and error output channels to any object you want, as long as it has a
write() function that accepts a string. We might want to add a write() function
to KalamDoc.
The implementation of write() in KalamDoc is very simple:
# kalamdoc.py - fragment
...
def write(self, text, view = None):
self.text().append(text)
self.emit(PYSIGNAL("sigDocTextChanged"),
(self._text, view))
444
Chapter 20. A Macro Language for Kalam
...
def slotMacroExecuteDocument(self):
...
import sys
docu-
ment = self.docManager.createDocument(KalamDoc, KalamView)
document.setTitle("Output of " + title)
oldstdout = sys.stdout
oldstderr = sys.stderr
sys.stdout = document
sys.stderr = document
sys.stdout = oldstdout
sys.stderr = oldstderr
...
It is necessary to save the "real" standard output and standard error channels in
order to be able to restore them when we are done printing to the output document.
Otherwise all output, from anywhere inside Kalam, would go forever to that
document, with nasty consequences if the user were to remove the document.
Until we create a namespace specially for executing macros, everything runs locally
to the function that executes the macro. That is, you can use self to refer to the
current instance of KalamApp.
445
Chapter 20. A Macro Language for Kalam
Of course, littering the KalamApp with macro execution code isnt the best of ideas.
This leads us to the creation of a macro manager class, MacroManager, which
keeps a dictionary of compiled code objects that can be executed at will. I wont
show the unit tests here: it is available with the full source code.
"""
macromanager.py -
manager class for macro administration and execution
from qt import *
import sys
class MacroError(Exception):pass
class NoSuchMacroError(MacroError):
def __repr__(self):
446
Chapter 20. A Macro Language for Kalam
return self.errorMessage
def __str__(self):
return self.errorMessage
class CompilationError(MacroError):
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
class ExecutionError(MacroError):
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
First, a couple of exceptions are defined. We want to separate the GUI handling of
problems with the macro from the actual execution, so that whenever something
goes wrong, an exception is thrown.
class MacroAction(QAction):
447
Chapter 20. A Macro Language for Kalam
self.code = code
self.bytecode = self.__compile(code)
self.locations=[]
self.connect(self,
SIGNAL("activated()"),
self.activated)
def activated(self):
self.emit(PYSIGNAL("activated"),(self,))
def remove(self):
for widget in self.locations:
self.removeFrom(widget)
sys.stdout = out
sys.stderr = err
exec self.bytecode in globals
sys.stdout = oldstdout
sys.stderr = oldstderr
448
Chapter 20. A Macro Language for Kalam
except Exception, e:
print e
print sys.exc_info
sys.stdout = oldstdout
sys.stderr = oldstderr
raise ExecutionError(e)
class MacroManager(QObject):
self.macroObjects = {}
if g == None:
self.globals = globals()
else:
self.globals = g
if l == None:
self.locals = locals()
449
Chapter 20. A Macro Language for Kalam
else:
self.locals = l
All macros should be executed in the same environment, which is why the
macromanager can be constructed with a globals and a locals environment.
This environment will be used later to create a special API for the macro execution
environment, and it will include access to the window (i.e. the KalamApp object)
and to the document objects (via the DocManager object).
The rest of the MacroManager class is simple, including methods to delete and add
macros, and to execute a named macro. Note how the activated signal of the
MacroAction is connected to the executeAction slot. This slot then calls
execute() on the macro action with standard output and standard error as default
output channels. A macro can, of course, create a new document and divert output
to that document.
The MacroManager is instantiated as part of the startup process of the main
application:
# kalamapp.py
def initMacroManager(self):
g=globals()
450
Chapter 20. A Macro Language for Kalam
self.macroManager = MacroManager(self, g)
Initializing the macromanager will also entail deciding upon a good API for the
macro extensions. This will be covered in a later section.
Adapting the slotMacroExecuteDocument() slot function to use the
MacroManager is quite straightforward:
# kalamapp.py
def slotMacroExecuteDocument(self):
if self.docManager.activeDocument() == None:
QMessageBox.critical(self,
"Kalam",
"No document to exe-
cute as a macro ")
return
title = self.docManager.activeDocument().title()
try:
macro-
Text = str(self.docManager.activeDocument().text())
self.macroManager.addMacro(title, macroText)
except CompilationError, e:
QMessageBox.critical(self,
"Kalam",
"Could not compile " +
self.docManager.activeDocument().title()
"\n" + str(e))
return
try:
doc, view = self.docManager.createDocument(KalamDoc, KalamView
doc.setTitle("Output of " + title)
self.macroManager.executeMacro(title, doc, doc)
except NoSuchMacroError, e:
QMessageBox.critical(self,
"Kalam",
"Error: could not find execu-
tion code.")
451
Chapter 20. A Macro Language for Kalam
except ExecutionError, e:
QMessageBox.critical(self,
"Kalam",
"Error executing " + title +
"\n" + str(e))
except Exception, e:
QMessageBox.critical(self,
"Kalam",
"Unpleasant er-
ror %s when trying to run %s." \
% (str(e), title))
Note the careful handling of exceptions. You dont want your application to crash or
become unstable because of a silly error in a macro.
# kalamapp.py - fragment
...
class KalamApp(QMainWindow):
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
452
Chapter 20. A Macro Language for Kalam
...
# Run the startup macro script
self.runStartup()
...
def runStartup(self):
"""Run a Python script using the macro man-
ager. Which script is
run is defined in the configuration vari-
ables macrodir and startup.
It is already possible to do anything you want using these macro extensions, but life
can be made easier by providing shortcut functions: a special macro API. We will
create one in the next section. However, a serious macro writer would have to buy a
copy of this book in order to be able to use all functionality, because hiding the
underlying GUI toolkit would remove far too much power from his hands.
453
Chapter 20. A Macro Language for Kalam
def initMacroManager(self):
g=globals()
g["kalam"]=self
self.macroManager = MacroManager(self, g)
Another, and perhaps slightly less hackish way of adding items to the global
namespace is the use of the global keyword:
def initMacroManager(self):
global kalam
kalam = self
self.macroManager = MacroManager(self, globals())
454
Chapter 20. A Macro Language for Kalam
# kalamapp.py
#
# Macro API
#
def installMacro(self,
action,
menubar = None,
toolbar = None):
"""
Installs a certain macro ac-
tion in the menu and/or the toolbar
"""
if menubar != None:
action.addTo(menubar)
if toolbar != None:
action.addTo(toolbar)
def createDocument(self):
doc, view = self.docManager.createDocument(KalamDoc, KalamView)
return (doc, view)
These methods are part of the KalamApp class, but it would be nice to not have to
prefix class with kalam from every macro. So these functions are added to the
global namespace, too:
def initMacroManager(self):
g=globals()
g["kalam"]=self
g["docManager"]=self.docManager
g["workspace"]=self.workspace
g["installMacro"]=self.installMacro
455
Chapter 20. A Macro Language for Kalam
g["removeMacro"]=self.removeMacro
g["createDocument"]=self.createDocument
g["createMacro"]=self.createMacro
self.macroManager = MacroManager(self, g)
Later, we will be writing a nice macro that resides in a file called edmund.py.
Heres how the startup.py script uses the API to install the macro:
#
# startup.py - Kalam startup macro file"
#
edmund = createMacro("edmund", open("edmund.py").read())
edmund.setMenuText("Edmund")
edmund.setText("Edmund")
edmund.setToolTip("Psychoanalyze Edmund")
edmund.setStatusTip("Psychoanalyze Edmund")
installMacro(edmund, kalam.macroMenu)
Using the kalam instance of KalamApp, the macro writer has access to all menus.
In this case the edmund macro is added to the macro menu, kalam.macroMenu .
456
Chapter 20. A Macro Language for Kalam
By not hiding the underlying gui toolkit, clever users can do almost anything to
your application. It would be a trivial exercise to integrate a Python class browser
into Kalam, especially since I have already made a PyQt based standalone class
browser, which you can find at http://www.valdyas.org/python. However, lets not
be so serious and sensible, and implement something a little more frivolous.
import random
class Edmund(QObject):
"""
An Edmund macro for the Kalam Editor.
457
Chapter 20. A Macro Language for Kalam
458
Chapter 20. A Macro Language for Kalam
self.respond)
self.view.append("Welcome\n")
self.view.goEnd()
def respond(self):
input = str(self.view.textLine(self.view.numLines() -
2))
if input.find("love") > 0:
response = self.responses[3]
elif input.find("dead") > 0:
response = self.responses[15]
elif input.find("fear") > 0:
response = self.responses[5]
else:
choice = random.randrange(0,len(self.responses),1)
response = self.responses[choice]
self.view.append(response + "\n\n")
self.view.goEnd()
edmund = Edmund()
Of course, this is an extremely primitive form of amusement, but you get the idea.
By accessing the APIs of the KalamDoc and KalamView classes, the macro author
can do all kinds of fun things, like reading out lines of the text or adding text to the
document.
459
Chapter 20. A Macro Language for Kalam
20.4. Conclusion
Adding Python as a macro language to an application has been fun, and has given
the user a lot of power. If you want to restrict your users, you might want to
investigate the bastion and restricted execution environments that Python offers.
460
Chapter 21. Drawing on Painters and
Canvases
Constructing windows out of predefined widgets is all very nice, but the real
exciting stuff occurs when you have to leave the beaten track and push the pixels
around yourself. This happens when you want to create diagram editors, drawing
applications, games (especially games!), complex page layout engines (like an html
renderer), charts, and plots.
Python and PyQt form a good basis for this kind of work, because Qt already has
two very powerful and optimized drawing engines: the QCanvas class and the
QPainter. QCanvas is extremely useful if your drawing can be composed from
separate elements, and if you want to be able to track events that occur on those
individual elements, such as mouse clicks.
QPainter offers finer control of what you put on screen, at the cost of losing
control over the individual elements that make up the drawing. QPainter is more
suited to drawing charts, bit-mapped drawings and plots. If you want real plotting
power, you should investigate PyQwt, which is introduced in Appendix B
Both QCanvas and QPainter are very powerful. In fact, they are even used in
assembling software used to create animated films. Animation means a lot of
intensive work for your computer, though, and Python can not always copeeven
on the most modern machines. In such cases, it is quite easy to replace the Python
class with a Qt-based C++ object (you wont have to translate your whole Python
application). See Appendix C for information on the wrapping of new C++ objects
with sip.
461
Chapter 21. Drawing on Painters and Canvases
operations. It provides the brushes and the letterpress, so to speak. The second
provides the paper on which to draw.
There are four subclasses of QPaintDevice: QWidget, QPicture, QPixMap and
QPrinter.
You use hand-coded painting with a QPainter object on a QWidget to determine
the look of a widget. The place for the painting code is in re-implementations of the
paintEvent() function.
QPicture is a kind of event recorder: it records every QPainter action, and can
replay them. You can also save those actions to a platform independent file. This is
useful if you want to implement rolling charts with a limited replay functionality
(although I would prefer to save the underlying data and reconstruct the chart every
time). You cannot alter anything in the sequence of events once it is recorded.
Starting with Qt 3, QPicture has become quite powerful, with the ability to load
and save industry standard .svg files - the scalable vector graphics format.
Painting on a QPixMap is extraordinarily useful. Painting is always a bit slow,
especially if it is done line by line, dot by dot, and character by character. This can
result in visible lag or flickering if you paint directly on an exposed QWidget. By
first painting the complete drawing on a QPixMap object, and then using the
bitBlt() function to move the picture in one swoop the the widget, you will avoid
this flickering. bitBlt() really is fast.
Finally, being able to paint on a QPrinter object means that anything you can
draw on-screen can also be printed. However, printing is still quite a difficult subject
even if PyQt can generate your PostScript for you, you still have to layout
everything yourself. You cannot, for instance, send the contents of a
QSimpleRichText widget to a printer just like that... Well discuss the basics of
printing in Chapter 24.
462
Chapter 21. Drawing on Painters and Canvases
"""
typometer.py
A silly type-o-
meter that keeps a running count of how many characters there
are in a certain document and shows a chart of the count...
"""
import sys, whrandom
from qt import *
TRUE=1
FALSE=0
class TypoGraph(QPixmap):
""" TypoGraph is a sub-
class of QPixmap and draws a small graph of
the current wordcount of a text.
"""
def __init__(self, count, w, h, *args):
apply(QPixmap.__init__, (self, w, h) + args)
self.count = count
self.maxCount = AVERAGE_TYPESPEED
if count != 0:
self.scale = float(h) / float(count)
else:
self.scale = float(h) / float(AVERAGE_TYPESPEED)
self.col = 0
self.fill(QColor("white"))
463
Chapter 21. Drawing on Painters and Canvases
self.drawGrid()
The general design of this chart drawing code consists of two parties: a specialized
pixmap, descended from QPixmap, that will draw the chart and keep track of
scrolling, and a widget that show the chart and can be used everywhere where you
might want to use a widget.
In the constructor of TypoGraph, the specialized QPixMap, certain initial variables
are set. One point of attention is scaling. The chart will have a certain fixed vertical
size. It is quite possible that the plotted values wont fit into the available pixels.
This means that we have to scale the values to fit the pixels of the chart. This is done
by arbitrarily deciding upon a maximum value, and dividing the height of the chart
by that value. Any value greater than the maximum will go off the chart, but if you
can type more than 125 characters in five seconds, you deserve to fly off the chart!
Because the scaling can be smaller than one but greater than zero, we need to use
float numbers for our scale. Floats are notoriously slow, but believe me, your
computer can handle more floats than you can throw at it per second, so you wont
feel the penalty for not using integers.
Finally, we fill the pixmap with a background color (white in this case) and draw a
nice grid:
def drawGrid(self):
p = QPainter(self)
p.setBackgroundColor(QColor("white"))
h = self.height()
w = self.width()
for i in range(1, h, h/5):
p.setPen(QColor("lightgray"))
p.drawLine(0, i, w, i)
This is the first encounter with QPainter. The basic procedure for working with
painter objects is very simple: you create a painter for the right paintdevice. Here
the paintdevice is self our specialized QPixMap. After having created the
QPainter you can mess about drawing lines, setting colors or throwing more
complex shapes on the paper. Here, we draw four lines at equal distances using a
464
Chapter 21. Drawing on Painters and Canvases
light-gray pen. The distance is computed by letting the range function use the
height of the widget divided by the number of rows we want as a stepsize.
If you wish to use several different painter objects, you might want to use the
begin() and end() methods of the QPainter class. In normal use, as here, the
begin() function is called when the QPainter is created, and end() when it is
destroyed. However, because the reference goes out of scope, end() is called
automatically, so you wont have to call end() yourself.
def text(self):
return QString(str(self.count))
The function text() simply returns a QString object containing the last plotted
value. We will use this to set the caption of the chart window.
h = self.height()
w = self.width()
p = QPainter(self)
p.setBackgroundColor(QColor("white"))
p.setBrush(QColor("black"))
if self.col >= w:
self.col = w
# move one pixel to the left
pixmap = QPixmap(w, h)
pixmap.fill(QColor("white"))
bitBlt(pixmap, 0, 0,
self, BARWIDTH, 0, w - BARWIDTH, h)
bitBlt(self, 0, 0, pixmap, 0, 0, w, h)
for i in range(1, h, h/5):
p.setPen(QColor("lightgray"))
465
Chapter 21. Drawing on Painters and Canvases
p.drawLine(self.col - BARWIDTH , i, w, i)
else:
self.col += BARWIDTH
y = float(self.scale) * float(self.count)
# to avoid ZeroDivisionError
if y == 0: y = 1
# Draw gradient
minV = 255
H = 0
S = 255
vStep = float(float(128)/float(y))
for i in range(y):
color = QColor()
color.setHsv(H, S, 100 + int(vStep * i))
p.setPen(QPen(color))
p.drawLine(self.col - BARWIDTH, h-i, self.col, h-
i)
The update() function is where the real meat of the charting pixmap is. It draws a
gradiented bar that scrolls left when the right side is reached (that is, if the current
column has arrived at or gone beyond the width of the pixmap).
The scrolling is done by creating a new, empty QPixmap and blitting the right hand
part of the old pixmap onto it. When writing this code, I noticed that you cannot blit
a pixmap onto itself. So, after weve created a pixmap that contains the old pixmap
minus the first few vertical lines, we blit it back, and add the grid to the now empty
right hand side of the pixmap.
The height of the bar we want to draw is computed by multiplying the value
(self.count) with the scale of the chart. If the result is 0, we make it 1.
We draw the bar in steps, with each step having a subtly differing color from the one
before it. The color gradient is determined by going along the value range of a
hue-saturation-value color model. Value determines darkness, with 0 being
completely dark, and 255 completely light. We dont use the complete range, but
step directly from 100 (fairly dark) to 228 (quite bright). The step is computed by
466
Chapter 21. Drawing on Painters and Canvases
dividing the value range we want (128) by the height of the bar. Every bar is going
from 100 to 228.
Then we step through the computed height of the bar, drawing a horizontal line with
the length of the bar thickness BARWIDTH.
Computing gradients is fairly costly, but it is still possible to type comfortably when
this chart is running: a testimony to the efficient design of QPainter. If your needs
are more complicated, then QPainter offers a host of sophisticated drawing
primitives (and not so primitives, like shearing, scaling, resizing and the drawing of
quad beziers).
The TypoGraph is completely generic: it draws a nicely gradiented graph of any
values that you feed the update function. Theres some testing code included that
uses a simple timer to update the chart with a random value.
A stand-alone chart
More application-specific is the TypoMeter widget, which keeps track of all open
Kalam documents, and shows the right chart for the currently active document.
class TypoMeter(QWidget):
self.docmanager = docmanager
self.workspace = workspace
self.resize(w, h)
self.setMinimumSize(w,h)
self.setMaximumSize(w,h)
467
Chapter 21. Drawing on Painters and Canvases
self.h = h
self.w = w
self.connect(self.docmanager,
PYSIGNAL("sigNewDocument"),
self.addGraph)
self.connect(self.workspace,
PYSIGNAL("sigViewActivated"),
self.changeGraph)
self.graphMap = {}
self.addGraph(self.docmanager.activeDocument(),
self.workspace.activeWindow())
self.timer = QTimer(self)
self.connect(self.timer,
SIGNAL("timeout()"),
self.updateGraph)
self.timer.start(FIVE_SECONDS, FALSE)
In order to implement this feature, some new signals had to be added to the
document manager and the workspace classes. Note also the use of the QTimer
class. A timer is created with the current object as its parent; a slot is connected to
the timeout() signal, and the timer is started with a certain interval. The FALSE
parameter means that the timer is supposed to keep running, instead of firing once,
when the timeout is reached.
468
Chapter 21. Drawing on Painters and Canvases
0, 0,
self.w,
self.h)
def updateGraph(self):
prevCount = self.graphMap[self.currentDocument][1]
newCount = self.currentDocument.text().length()
self.graphMap[self.currentDocument] = (self.currentGraph, newCount
self.currentGraph.update(delta)
bitBlt(self, 0, 0,
self.currentGraph,
0, 0,
self.w,
self.h)
self.setCaption(self.currentGraph.text())
The actual keeping track of the type-rate is done in this class, not in the TypoChart
class. In making good use of Pythons ability to form tuples on the fly, a
combination of the TypoChart instance and the last count is kept in a dictionary,
indexed by the document.
Using the last count and the current length of the text, the delta (the difference) is
computed and fed to the chart. This updates the chart, and the chart is then blitted
onto the widget a QWidget is a paintdevice, after all.
469
Chapter 21. Drawing on Painters and Canvases
class TestWidget(QWidget):
if __name__ == __main__:
a = QApplication(sys.argv)
QObject.connect(a,SIGNAL(lastWindowClosed()),a,SLOT(quit()))
w = TestWidget()
a.setMainWidget(w)
w.show()
a.exec_loop()
Finally, this is some testing code, not for the TypoMeter class, which can only
work together with Kalam, but for the TypoChart class. It is difficult to use the
unit testing framework from Chapter 14 here after all, in the case of graphics
work, the proof of the pudding is in the eating, and its difficult to assert things
about pixels on the screen.
The code to show the type-o-meter on screen is interesting, since it shows how you
can destructively delete a widget. The QAction that provides the menu option
"show type-o-meter" is a toggle action, and changing the toggle emits the
toggled(bool) signal. This is connected to the following function (in
kalamapp.py:
470
Chapter 21. Drawing on Painters and Canvases
self.typowindow = TypoMeter(self.docManager,
self.workspace,
100,
100,
self,
"type-o-meter",
Qt.WType_TopLevel or Qt.WDestructi
self.typowindow.setCaption("Type-o-meter")
self.typowindow.show()
else:
self.typowindow.close(TRUE)
21.2. QCanvas
The other way of pushing pixels on the screen is using the QCanvas class. This is
rather more complicated than simply painting what you want, but offers the unique
capability of accessing the individual elements of the composition. Not only that,
but you can also determine whether elements overlap each other, set them moving
across the canvas at a predefined rate, and show and hide them at will.
In working with QCanvas, three classes play an essential role: the QCanvas itself,
which is a receptacle for QCanvasItem objects or rather, their descendants, and
one or more QCanvasView widgets are used to show the canvas and its contents on
screen.
471
Chapter 21. Drawing on Painters and Canvases
The class QCanvasItem is rather special: you cannot instantiate objects from it, nor
can you directly subclass it. You can instantiate and subclass the subclasses of
QCanvasItem: QCanvasPolygonalItem , QCanvasSprite and QCanvasText.
472
Chapter 21. Drawing on Painters and Canvases
A QCanvasSprite should be familiar to anyone who has ever played with an 8-bit
home computer. A QCanvasSprite is an animated pixmap, and can move (like
any QCanvasItem) across the canvas under its own steam. You fill the
QCanvasSprite with a QPixMapArray. This class contains a list of QPixmaps
and a list of QPoints. These define how the sprite looks and where its hot spots are.
If you want to create a game using PyQt youll probably want to use this class.
Lastly, the QCanvasText can draw a single line of text on the canvas. Let me repeat
that: you can not create a whole column of text, put it in a QCanvasText object,
and paste it on the canvas. This makes creating a PageMaker clone just a little bit
more difficult.
Nevertheless, it is QCanvasText which we are going to use in the next section.
Another example of the use QCanvasText is the Eric debugger, which is part of the
PyQt source distribution.
473
Chapter 21. Drawing on Painters and Canvases
from the complete unicode range in Kalam. The Unicode range is divided into a few
hundred scripts. What I want is a window that shows a clickable table of one of
those scripts, with a combo-box that allows me to select the script I need. And when
I click on a character, that character should be inserted into the current document.
The underlying data can be retrieved from the Unicode consortium website. They
provide a file, Blocks.txt, that gives you the range each script occupies:
This file can be used to fill a combobox with all different scripts:
474
Chapter 21. Drawing on Painters and Canvases
"""
charmap.py - A unicode character selector
TRUE=1
FALSE=0
class CharsetSelector(QComboBox):
f=open(os.path.join(datadir,"Blocks.txt"))
f.readline() # skip first line
for line in f.readlines():
try:
self.charsets.append((string.atoi(line[0:4],16)
,string.atoi(line[6:10],16)))
self.insertItem(line[12:-1])
except: pass
This is simple enough: the location of Blocks.txt is retrieved, and each line is
read. Every line represents one script, and for every line an entry is inserted into the
QComboBox. In a separate list, self.charsets, we keep a tuple with the begin
475
Chapter 21. Drawing on Painters and Canvases
and the end of each range, converted to integers from their hexadecimal
representation. Python is a great language for this kind of data massaging.
Whenever the user selects an item from the combobox, a signal is emitted,
sigActivated, that carries the begin and endpoint of the range.
class CharsetCanvas(QCanvas):
fontMetrics=QFontMetrics(self.font)
476
Chapter 21. Drawing on Painters and Canvases
cell_width=fontMetrics.maxWidth() + 3
if self.maxW < 16 * cell_width:
self.maxW = 16 * cell_width
cell_height=fontMetrics.lineSpacing()
item.setX(x)
item.setY(y)
item.show()
self.items.append(item)
x=x + cell_width
if x >= self.maxW:
x=0
y=y+cell_height
self.resize(self.maxW + 20, h)
self.update()
Most of the real work is done in the drawTable() method. The maxW parameter
determines how wide the canvas will be. However, if there is not place enough for at
least sixteen glyphs, the width is adjusted.
Then the QCanvasText items are created, in a plain loop, starting at the beginning
of the character set and running to the end. You must give these items an initial
position and size, and explicitly call show() on each item. If you forget to do this,
all you will see is a very empty canvas.
477
Chapter 21. Drawing on Painters and Canvases
You will also be greeted by an equally empty canvas if you do not keep a Python
reference to the items here a list of QCanvasText items is kept in self.items.
If the end of a line is reached, drawing continues on the next line.
An essential step, and one which I tend to forget myself, is to resize the QCanvas
after having determined what space the items take. You can place items outside the
confines of the canvas, and they wont show unless you resize the canvas to include
them.
Finally, you must update() the QCanvas otherwise you still wont see
anything. This method updates all QCanvasView objects that show this canvas.
Setting the font involves drawing the table anew. This is more efficient than
applying the font change to each individual QCanvasText item even though that
is perfectly possible. The reason is that if the font metrics change, for instance
because the new font is a lot larger, you will have to check for collisions and adjust
the location of all items anyway. That would take not only a lot of time, it would
also demand complex and unmaintainable code. Simple is good, as far as Im
concerned.
This little table shows almost nothing of the power of QCanvas you can animate
the objects, determine if they overlap, and lots more. It offers everything you need,
for instance, to write your very own Asteroids clone...
class CharsetBrowser(QCanvasView):
478
Chapter 21. Drawing on Painters and Canvases
self.cursorItem.setZ(-1.0)
self.cursorItem.setPen(QPen(QColor(Qt.gray), 2, Qt.DashLine))
self.cursorItem.show()
self.canvas().update()
First, the drawing of the cursor. You can see that you dont need to create your
canvas items in the QCanvas class or its derivatives. Here, it is done in the
setCursor() method. This method is called with the activated QCanvasText
item as its parameter.
A new item is created, a QCanvasRectangle called self.cursorItem . Its an
instance, not a local variable, because otherwise the rectangle would disappear once
the item goes out of scope (because the function finishes).
The location and dimensions of the rectangle are determined. It will be a two-pixel
wide, gray, dashed line exactly outside the current glyph. Of course, it must be
shown, and the canvas must call update() in order to notify the view(s). Note that
you can retrieve a canvas shown by QCanvasView with the canvas() function.
If you consult PyQts documentation (or the C++ Qt documentation) on
QCanvasView, you will notice that it is not very well endowed with useful
479
Chapter 21. Drawing on Painters and Canvases
class CharMap(QWidget):
def __init__(self,
parent,
initialFont = "arial",
datadir = "unidata",
*args):
480
Chapter 21. Drawing on Painters and Canvases
self.box.addWidget(self.comboCharset)
self.charsetCanvas=CharsetCanvas(self, self.font, 0, 0, 0)
self.charsetBrowser=CharsetBrowser(self.charsetCanvas, self)
self.box.addWidget(self.charsetBrowser)
self.connect(qApp,
PYSIGNAL("sigtextfontChanged"),
self.setFont)
self.connect(self.comboCharset,
PYSIGNAL("sigActivated"),
self.slotShowCharset)
self.connect(self.charsetBrowser,
PYSIGNAL("sigMousePressedOn"),
self.sigCharacterSelected)
self.resize(300,300)
self.comboCharset.sigActivated(self.comboCharset.currentItem())
In the constructor of CharMap both the selector combobox and the canvasview are
created. We create an initial canvas for the view to display. The
qApp.sigtextfontChanged signal is used to redraw the character map when the
application font changes. Recall how we synthesized signals for all configuration
options in Chapter 18, and used the globally available qApp object to emit those
signals.
481
Chapter 21. Drawing on Painters and Canvases
self.charTable=CharsetCanvas(self,
self.font,
begin,
end,
self.width() - 40)
self.charsetBrowser.setCanvas(self.charTable)
self.setCursor(Qt.arrowCursor)
Drawing a character map can take a while, especially if you select the set of
Chinese characters, which has a few tens of thousands of entries. In order to not
disquiet the user, we set a waiting cursorthis is a small wristwatch on most
versions of Unix/X11, and the familiar sand-timer on Windows. Then a new canvas
is created and the canvas view is told to display it.
Input methods and foreign keyboards: If you have played around with the
version of Kalam that belongs to this chapter, you will no doubt have noticed
that writing a letter in, say, Tibetan, is not quite as easy as just banging on the
keyboard (to say nothing of writing Chinese, which demands advanced
hunting and picking skills).
A character map like we just made is useful for the occasional phonetic or
mathematics character, but not a substitute for the real stuff: specific keyboard
482
Chapter 21. Drawing on Painters and Canvases
layouts for alphabetic scripts, like Cyrillic or Thai, and input method editors for
languages like Chinese.
Properly speaking, its the job of the Operating System or the GUI system to
provide this functionality. Specialized keyboard layouts are fairly easy to come
by, at least in the Unix/X11 world. My KDE 2 desktop has lots of keyboard
layouts perhaps you have to buy them in the Windows world. Still, its not
worthwhile to create special keyboard layouts in PyQt.
It is possible to create your own keyboard layouts in PyQt: re-implement the
keyPressEvent() of the view class and use each pressed key as an index
into a dictionary that maps plain keyboard key definitions to, say, Tibetan
Unicode characters. This is the same technique we used in Chapter 17 to
make sure tab characters ended up in the text
keymap={Qt.Key_A: QString(u"\u0270")}
Input method editors (IMEs) are more difficult. Installing the free Chinese or
Japanese IMEs on Unix/X11 is a serious challenge. Getting your applications
to work with them is another challenge. There are, however, special Chinese,
Korean and Japanese versions of Qt to deal with these problems. As for
Windows, I think you need a special Chinese, Korean or Japanese version of
Windows.
It can be worthwhile to implement a Chinese IME, for instance, yourself:
483
Chapter 21. Drawing on Painters and Canvases
You can find the code for a stand-alone Pinyin-based Chinese IME at
http://www.valdyas.org/python/qt2.html its also a nice example of using
large Python dictionaries (every Mandarin Chinese syllable is mapped to a list
characters with that pronunciation, and Emacs cannot syntax-color the file
containing the dictionary).
21.3. Conclusion
In this chapter we briefly investigated two ways of creating two-dimensional
graphics with Python and Qt. If you possess a suitable 3d accelerated graphics card,
you can also use the OpenGL Qt extensions from Python and create complex
three-dimensional scenes with PyQt. I cant help you with that task Ive never
worked with three-dimensional graphics myself, and besides, my computer isnt up
to it.
484
Chapter 22. Gui Design in the
Baroque Age
One of the greatest achievements of GUI interfaces such as CDE, Windows or
MacOS is the uniform look and feel of the applications. Once a user has learned to
use one application effectively, he will be familiar with the way all applications
work on that platform. Of course, there are also users who complain they forget
exactly which application they are working with when all applications look the
same. Be that as it may, the current trend, perhaps initiated with the wide variety of
design for websites, is toward ever-more distinctive and baroque interfaces. Indeed,
we seem to be entering the age of the designer interface, and PyQt offers
everything we need to create database applications in the form of an audio rack or
spreadsheets in the form of a medieval book of hours.
485
Chapter 22. Gui Design in the Baroque Age
environment.
There are two ways to create a totally custom interface. You can reimplement
QWidget using pixmaps and masks, or you can use the PyQt QStyle mechanism to
reimplement the drawing routines of the standard widgets like QPushButton.
The first solution is applicable for simple applications like audio players or for
applications that need to be totally skinnable (again, like audio players). You need
also to reimplement QWidget if you have designed a totally new type of widget,
like Adobe did with the PageMager paste-board. (You can also simulate the
pasteboard quite easily with QCanvas, but you might have an idea for a widget
thats more complicated than this.)
486
Chapter 22. Gui Design in the Baroque Age
A scan of the remote control. Thanks to Cameron Laird for permission to use this
image.
Note the subtle play of light on the buttons: no two buttons are the same. The
photo-realistic effect would be very hard to achieve without using an actual
photograph. This means that we will have to cut out each separate button, and save
it in a separate file. The buttons should give feedback to the user when depressed, so
we make a copy of each button and save that, too. Then, using a bitmap
manipulation program such as the Gimp, the highlight in the up button is changed to
a bit of shadow, and the text is shifted a few pixels to the right and left.
487
Chapter 22. Gui Design in the Baroque Age
This part of the proceedings is a bit of a bore: lets skip drawing a few of those
buttons, and start with the implementation. A real remote control doesnt have
window borders, so ours shouldnt have those, either. We can achieve this effect by
using window flags: first the Qt.WStyle_Customize flag to indicate that we are
customizing the appearance of the window, and then the Qt.WStyle_NoBorderEx
flag to tell the window system that we dont want borders.
#
# remote.py - remote control application
#
class Window(QMainWindow):
def initView(self):
488
Chapter 22. Gui Design in the Baroque Age
def main(argv):
app=QApplication(sys.argv)
window=Window()
window.show()
app.connect(app, SIG-
NAL(lastWindowClosed()), app, SLOT(quit()))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Example 22-2. view.py - the main view of the remote control application
#
# view.py = the main view to go with remote.py
#
from qt import *
from button import Button
class View(QWidget):
buttondefs=[ (register, 80, 111)
, (freeze, 22, 388)
, (minus, 22, 457)
, (toolbox, 130 , 166)]
489
Chapter 22. Gui Design in the Baroque Age
self.buttons.append(bn)
QObject.connect(bn, PYSIGNAL("pressed"), self.Pressed)
There is a class variable buttondef that contains the set of buttons we will use.
Each definition consists of a base name from which the filenames of the up and
down buttons will be deduced, and the X and Y position of the button on the
window. These hardcoded positions make it impossible to use this technique
together with layout managers.
A background image is set in the constructor, using setBackgroundPixmap ; after
this, all actual button objects are created. The button objects are instances of the
button class:
Example 22-3. button.py - the class that implements the pixmapped buttons
#
# button.py -
pixmapped button definition to go with remote.py
#
from qt import *
class Button(QWidget):
def __init__(self, parent, name, x, y, up, down, *args):
apply(QWidget.__init__,(self, parent)+args)
self.pxUp=QPixmap(up)
self.pxDown=QPixmap(down)
self.setBackgroundPixmap(self.pxUp)
self.name=name
self.x=x
self.y=y
self.move(x, y)
self.setFixedSize(self.pxUp.size())
490
Chapter 22. Gui Design in the Baroque Age
self.emit(PYSIGNAL("pressed"), (self.name, ))
The button class is interesting because of its extreme simplicity. All it really does is
move itself to the right place and then wait for mouse clicks. When a mouse button
is pressed, the down pixmap is shown, using setBackgroundPixmap() . When the
mouse button is released, the up pixmap is restored. In order to be able to catch the
press event in view.py, a signal is generated.
Creating a set of graphics for the number displays and updating those when the
buttons are pressed has been left out of this book. (Its extremely tedious, Im afraid
to say).
Using pixmaps to create a distinctive user interface works well for smaller projects,
and in situations where every detail has to be just right. However, the tedium
involved in creating the individual pixmaps, and the lack of flexibility, makes the
technique unsuitable for more complex interfaces.
491
Chapter 22. Gui Design in the Baroque Age
then, designing a user interface calls for the same expertise. Thats the reason most
software houses employ interaction designers and graphic artists. Software
developers who dont have access to these specialists can refer to the excellent
books on interface design published by Apple and Microsoft. These are the
Macintosh Human Interface Guidelines
(http://www.devworld.apple.com/techpubs/mac/HIGuidelines/HIGuidelines-2.html)
and Microsoft Guidelines for Interface design, respectively. The Interface Hall of
Shame website (http://www.iarchitect.com/mshame.htm) has some hilarious
examples of interface design!
On the other hand, making something look like something else is a lot easier. For
this project, Ive pondered a few example styles that would illustrate the capabilities
of the QStyle mechanism: the old Athena widget look and feel, or the old flat
MacOS look and feel. The new Aqua style was right out, for legal reasons. Perhaps
the look of the rubber keyboard or the old ZX-Spectrum? For my example, well try
the old, flat MacOS look. This one has the advantage of being visually simple. You
can complicate the implementation of a style enormously by using complex
gradients to fill buttons, or bitmaps and masks to fill widget backgrounds. The
QStyle is flexible enough that you can use anything the QPainter class offers to
paint your widgets. On the other hand, some parts are exceedingly difficult to adapt,
as we shall see.
22.3.2. Setting up
The system that is used to implement custom styles changed completely between Qt
2 and Qt 3, which is a pity, because at the time of writing this book the new styling
system had not been completely finished (and not wrapped for PyQt), but the old
system has been removed.
New styles are implemented by subclassing one of the descendants of QStyle (this
holds for both systems).
Under the old system, most of the widget drawing code would have been placed in
the polish() and unpolish() methods of the style implementation.
The new system demands that you re-implement the primitive drawing functions,
such as drawItem(), drawPrimitive() or drawComplexControl .
In both cases, you can decide to either use bitmaps for buttons and backgrounds.
492
Chapter 22. Gui Design in the Baroque Age
#
# macstyle.py -
A minimalist implementation of the Mac Classic style for PyQt
# and Qt 2.
# Use with styletester.py
#
from qt import *
import time
FALSE=0
TRUE=1
class MacStyle(QWindowsStyle):
def __init__(self):
QWindowsStyle.__init__(self)
self.setButtonDefaultIndicatorWidth(0)
if isinstance(object, QApplication):
self.polish_qapplication(object)
elif isinstance(object, QWidget):
493
Chapter 22. Gui Design in the Baroque Age
self.polish_qwidget(object)
else:
QPlatinumStyle.polish(self, object)
# color definitions
white=QColor("white")
lightgray=QColor(210,210,210)
gray=QColor(190,190,190)
darkgray=QColor(120,120,120)
black=QColor("black")
active=QColorGroup()
#
# Basic colors
#
active.setColor(QColorGroup.Background,
white) # general background color
494
Chapter 22. Gui Design in the Baroque Age
active.setColor(QColorGroup.Foreground,
black) # general foreground color
active.setColor(QColorGroup.Base,
white) # lighter back-
ground for text widgets
active.setColor(QColorGroup.Text,
black) # foreground to go with Base
active.setColor(QColorGroup.Button,
white) # button background color
active.setColor(QColorGroup.ButtonText,
black) # button text color
#
# Used for bevels and shadows
#
active.setColor(QColorGroup.Light,
lightgray ) # a bit lighter than Button
active.setColor(QColorGroup.Midlight,
gray)
active.setColor(QColorGroup.Dark,
darkgray) # depressed button state
active.setColor(QColorGroup.Mid,
gray) # mid tone
active.setColor(QColorGroup.Shadow,
black) # shadow tone
#
# Selections
#
active.setColor(QColorGroup.Highlight,
black)
active.setColor(QColorGroup.HighlightedText,
white)
#
# Text color that shows well on Dark
#
active.setColor(QColorGroup.BrightText,
white)
disabled=QColorGroup(active)
disabled.setColor(QColorGroup.Base, gray)
495
Chapter 22. Gui Design in the Baroque Age
disabled.setColor(QColorGroup.Text, darkgray)
inactive=QColorGroup(active)
inactive.setColor(QColorGroup.Text, darkgray)
self.newPalette=QPalette(active, disabled, inactive)
app.setPalette(self.newPalette, TRUE)
if w.inherits("QLCDNumber"):
return
if not w.isTopLevel():
if w.inherits("QLabel") \
or w.inherits("QButton") \
or w.inherits("QComboBox") \
or w.inherits("QGroupBox") \
or w.inherits("QSlider") \
or w.inherits("QTabWidget") \
or w.inherits("QPanel"):
w.setAutoMask(TRUE)
496
Chapter 22. Gui Design in the Baroque Age
if w.inherits("QLCDNumber"):
return
if not w.isTopLevel():
if w.inherits("QLabel") \
or w.inherits("QButton") \
or w.inherits("QComboBox") \
or w.inherits("QGroupBox") \
or w.inherits("QSlider") \
or w.inherits("QTabWidget") \
or w.inherits("QPanel"):
w.setAutoMask(FALSE)
#
# Panel, rectangles and lines
#
oldpen=painter.pen()
oldbrush=painter.brush()
if sunken:
painter.setPen(QPen(colorGroup.foreground(), 2, QPen.DotLine))
else:
painter.setPen(QPen(colorGroup.foreground(), 2))
if fill:
oldbrush=painter.brush()
painter.setPen(colorGroup.foreground())
painter.setBrush(fill)
painter.drawRect(x + 2, y + 2, w - 2, h - 2)
painter.setPen(oldpen)
497
Chapter 22. Gui Design in the Baroque Age
painter.setBrush(oldbrush)
def drawSepara-
tor(self, painter, x1, y1, x2, y2, colorGroup,
sunken, lineWidth, midLineWidth):
painter.save()
painter.setPen(colorGroup.foreground, lineWidth)
painter.drawLine(x1, y1, x2, y2)
painter.restore()
d=r*2-1
region.unite(QRegion(x, y, r*2, r*2, QRegion.Ellipse))
region.unite(QRegion(right -
d, y, r*2, r*2, QRegion.Ellipse))
region.unite(QRegion(x, bottom-
d, r*2, r*2, QRegion.Ellipse))
498
Chapter 22. Gui Design in the Baroque Age
region.unite(QRegion(right-d, bottom-
d, r*2, r*2, QRegion.Ellipse))
return region
#
# Tab
#
right=tab.r.width()-1
for i in range(5):
a.setPoint(9-i, right -
a.point(i)[0], a.point(i)[1])
for i in range(10):
a.setPoint(i, a.point(i)[0], tab.r.height() - 1 -
a.point(i)[1])
a.translate(tab.r.left(), tab.r.top())
if selected:
painter.setBrush(tabBar.colorGroup().background())
else:
painter.setBrush(tabBar.colorGroup().light())
499
Chapter 22. Gui Design in the Baroque Age
painter.setPen(tabBar.colorGroup().foreground())
painter.drawPolygon(a)
painter.setBrush(Qt.NoBrush)
#
# Sliders
#
color-
Group=QColorGroup(Qt.color1, Qt.color1, Qt.color1, Qt.color1,
Qt.color1, Qt.color1, Qt.color1, Qt.color1,
Qt.color0)
if orientation==Qt.Horizontal:
painter.fillRect(x, y, w, h, Qt.color1)
else:
painter.fillRect(x, y, w, h, Qt.color1)
#
# Buttons and pushbuttons
#
if fill != None:
500
Chapter 22. Gui Design in the Baroque Age
painter.setBrush(fill)
self.drawroundrect(painter, x, y, w, h)
painter.setBrush(oldBrush)
colorGroup=button.colorGroup()
(x1, y1, x2, y2)=button.rect().coords()
painter.setPen(colorGroup.foreground())
painter.setBrush(QBrush(colorGroup.button(),
Qt.NoBrush))
if button.isDown():
brush=QBrush()
brush.setColor(colorGroup.highlight())
brush.setStyle(QBrush.SolidPattern)
fill=brush
elif button.isOn():
brush=QBrush()
brush.setColor(colorGroup.mid())
brush.setStyle(QBrush.SolidPattern)
fill=brush
else:
fill=colorGroup.brush(colorGroup.Button)
if button.isDefault():
painter.setPen(QPen(Qt.black, 3))
self.drawroundrect(painter, x1, y1, x2-x1+1, y2-
y1+1)
painter.setPen(QPen(Qt.black, 1))
x1=x1+4
y1=y1+4
x2=x2-4
y2=y2-4
501
Chapter 22. Gui Design in the Baroque Age
if button.isOn() or button.isDown():
sunken=TRUE
else:
sunken=FALSE
if button.isMenuButton():
dx=(y1-y2-4)/3
self.drawArrow(painter, Qt.DownArrow, FALSE,
x2-dx, dx, y1, y2-y1,
colorGroup, button.isEnabled())
if painter.brush().style != Qt.NoBrush:
painter.setBrush(Qt.NoBrush)
x=x+2
y=y+2
w=w-4
h=h-4
g=button.colorGroup()
if button.isDown() or button.isOn():
pencolour=button.colorGroup().brightText()
else:
pencolour=button.colorGroup().buttonText()
self.drawItem(painter, x, y, w, h,
502
Chapter 22. Gui Design in the Baroque Age
Qt.AlignCenter|Qt.ShowPrefix,
g, button.isEnabled(),
button.pixmap(), button.text(), -1,
pencolour)
if dx or dy:
painter.translate(-dx,-dy)
#
# Radio Button
#
def drawExclusiveIndica-
tor(self, painter, x, y, w, h, colorGroup,
on, down, enabled):
painter.eraseRect(x, y, w, h)
painter.drawEllipse(x, y, w, h)
if on:
painter.setBrush(QBrush(colorGroup.foreground(), \
QBrush.SolidPattern))
painter.drawEllipse(x + 3, y + 3, w - 6, h -6)
def drawExclusiveIndicator-
Mask(self, painter, x, y, w, h, on):
painter.fillRect(x, y, w, h, QBrush(Qt.color1))
503
Chapter 22. Gui Design in the Baroque Age
#
# Checkbox
#
if enabled:
painter.setPen(QPen(colorGroup.foreground(), 1, \
QPen.SolidLine))
else:
painter.setPen(QPen(colorGroup.mid(), 1, QPen.SolidLine))
if state==QButton.Off:
painter.setBrush(QBrush(colorGroup.background(), \
QBrush.SolidPattern))
elif state==QButton.NoChange:
painter.setBrush(QBrush(colorGroup.dark(), \
QBrush.SolidPattern))
else:
painter.setBrush(QBrush(colorGroup.background(), \
QBrush.SolidPattern))
painter.drawRect(x, y, w, h)
if state==QButton.On:
painter.drawLine(x, y, x + w, y + h)
painter.drawLine(x, y + h - 1, x + w - 1, y)
painter.restore()
#
# Menu bar
#
504
Chapter 22. Gui Design in the Baroque Age
Not subclassable?
"""
self.drawItem(painter, x, y, w, h,
Qt.AlignCenter | Qt.ShowPrefix | Qt.DontClip | \
Qt.SingleLine, colorGroup, menu-
Item.pixmap(), \
menuItem.text(), -
1, QColorGroup.buttonText())
#
# These items are not (yet) implemented in PyQt
#
505
Chapter 22. Gui Design in the Baroque Age
#
# styletester.py - a testbed for styles.
# Based on Phils adaption of my translation of the
# Qt themes example app.
#
FALSE=0
TRUE=1
class ButtonsGroups(QVBox):
# first group
# insert 3 radiobuttons
506
Chapter 22. Gui Design in the Baroque Age
# second group
# insert 3 checkboxes
QCheckBox("&Checkbox 1", grp2)
cb12=QCheckBox("C&heckbox 2", grp2)
cb12.setChecked(TRUE)
cb13=QCheckBox("Triple &State Button", grp2)
cb13.setTristate(TRUE)
cb13.setChecked(TRUE)
# third group
507
Chapter 22. Gui Design in the Baroque Age
# insert a checkbox...
self.state=QCheckBox("E&nable Radiobuttons", grp3)
self.state.setChecked(TRUE)
# ...and connect its SIG-
NAL clicked() with the SLOT slotChangeGrp3State()
self.connect(self.state, SIGNAL(clicked()),self.slotChangeGrp3State)
# fourth group
def slotChangeGrp3State(self):
self.rb21.setEnabled(self.state.isChecked())
self.rb22.setEnabled(self.state.isChecked())
self.rb23.setEnabled(self.state.isChecked())
class LineEdits(QVBox):
self.setMargin(10)
508
Chapter 22. Gui Design in the Baroque Age
row1=QHBox(self)
row1.setMargin(5)
# Create a label
QLabel("Echo Mode: ", row1)
509
Chapter 22. Gui Design in the Baroque Age
self.lined1.setFocus()
self.lined2.setText("")
self.lined2.setFocus()
510
Chapter 22. Gui Design in the Baroque Age
if i == 0:
self.lined3.setAlignment(Qt.AlignLeft)
elif i == 1:
self.lined3.setAlignment(Qt.AlignCenter)
elif i == 2:
self.lined3.setAlignment(Qt.AlignRight)
self.lined3.setFocus()
class ProgressBar(QVBox):
self.timer=QTimer()
self.setMargin(10)
# Create a radiobutton-
exclusive Buttongroup which aligns its childs
# in two columns
bg=QButtonGroup(2, QGroupBox.Horizontal, self)
bg.setRadioButtonExclusive(TRUE)
511
Chapter 22. Gui Design in the Baroque Age
self.connect(self.start, SIG-
NAL(clicked()), self.slotStart)
self.connect(self.reset, SIG-
NAL(clicked()), self.slotReset)
def slotStart(self):
# If the progress bar is at the beginning...
if self.progress.progress() == -1:
# ...set according to the checked speed-
radionbutton the number of
# steps which are needed to complete the process
if self.slow.isChecked():
self.progress.setTotalSteps(10000)
elif self.normal.isChecked():
self.progress.setTotalSteps(1000)
else:
self.progress.setTotalSteps(50)
512
Chapter 22. Gui Design in the Baroque Age
def slotReset(self):
# stop the timer and progress
self.timer.stop()
def slotTimeout(self):
p = self.progress.progress()
class ListBoxCombo(QVBox):
513
Chapter 22. Gui Design in the Baroque Age
self.setMargin(5)
row1=QHBox(self)
row1.setMargin(5)
# Create a pushbutton...
self.arrow1=QPushButton(" -> ", row1)
# ...and connect the clicked SIG-
NAL with the SLOT slotLeft2Right
self.connect(self.arrow1, SIG-
NAL(clicked()), self.slotLeft2Right)
def slotLeft2Right(self):
# Go through all items of the first ListBox
for i in range(self.lb1.count()):
item=self.lb1.item(i)
# if the item is selected...
if item.selected():
# ...and it is a text item...
if not item.text().isEmpty():
# ...insert an item with the same text into the sec-
ond ListBox
self.lb2.insertItem(QListBoxText(item.text()))
# ...and if it is a pixmap item...
elif item.pixmap():
514
Chapter 22. Gui Design in the Baroque Age
# ...in-
sert an item with the same pixmap into the second ListBox
self.lb2.insertItem(QListBoxPixmap(item.pixmap()))
class Themes(QMainWindow):
self.appFont=QApplication.font()
self.tabwidget=QTabWidget(self)
self.buttonsgroups=ButtonsGroups(self.tabwidget)
self.tabwidget.addTab(self.buttonsgroups,"Buttons/Groups")
self.hbox=QHBox(self.tabwidget)
self.hbox.setMargin(5)
self.linedits=LineEdits(self.hbox)
self.progressbar=ProgressBar(self.hbox)
self.tabwidget.addTab(self.hbox, "Lineedits/Progressbar")
self.listboxcombo=ListBoxCombo(self.tabwidget)
self.tabwidget.addTab(self.listboxcombo, "Listboxes/Comboboxes")
self.setCentralWidget(self.tabwidget)
self.style=QPopupMenu(self)
self.style.setCheckable(TRUE)
self.menuBar().insertItem("&Style", self.style)
515
Chapter 22. Gui Design in the Baroque Age
self.help=QPopupMenu(self)
self.menuBar().insertSeparator()
self.menuBar().insertItem("&Help", self.help)
self.help.insertItem("&About", self.about, Qt.Key_F1)
self.help.insertItem("About &Qt", self.aboutQt)
self.style=MacStyle()
qApp.setStyle(self.style)
self.menuBar().setItemChecked(self.sMacStyle, TRUE)
def styleMac(self):
newstyle=MacStyle()
qApp.setStyle(newstyle)
self.style=newstyle
self.selectStyleMenu(self.sMacStyle)
def stylePlatinum(self):
newstyle=QPlatinumStyle()
qApp.setStyle(newstyle)
self.style=newstyle
p=QPalette(QColor(239, 239, 239))
qApp.setPalette(p, TRUE)
qApp.setFont(self.appFont, TRUE)
self.selectStyleMenu(self.sPlatinum)
def styleWindows(self):
newstyle=QWindowsStyle()
qApp.setStyle(newstyle)
self.style=newstyle
qApp.setFont(self.appFont, TRUE)
self.selectStyleMenu(self.sWindows)
def styleCDE(self):
newstyle=QCDEStyle(TRUE)
516
Chapter 22. Gui Design in the Baroque Age
qApp.setStyle(newstyle)
self.style=newstyle
self.selectStyleMenu(self.sCDE)
517
Chapter 22. Gui Design in the Baroque Age
qApp.setPalette(p, TRUE)
qApp.setFont(QFont("times", self.appFont.pointSize()), TRUE)
def styleMotif(self):
newstyle=QMotifStyle(TRUE)
qApp.setStyle(newstyle)
self.style=newstyle
p=QPalette(QColor(192, 192, 192))
qApp.setPalette(p, TRUE)
qApp.setFont(self.appFont, TRUE)
self.selectStyleMenu(self.sMotif)
def styleMotifPlus(self):
newstyle=QMotifPlusStyle(TRUE)
qApp.setStyle(newstyle)
self.style=newstyle
p=QPalette(QColor(192, 192, 192))
qApp.setPalette(p, TRUE)
qApp.setFont(self.appFont, TRUE)
self.selectStyleMenu(self.sMotifPlus)
def about(self):
QMessageBox.about(self, "Qt Themes Example",
"<p>This example demonstrates the concept of "
"<b>generalized GUI styles </b> first \
introduced "
" with the 2.0 release of Qt.</p>" )
def aboutQt(self):
QMessageBox.aboutQt(self, "Qt Themes Testbed")
518
Chapter 22. Gui Design in the Baroque Age
def main(argv):
QApplication.setColorSpec(QApplication.CustomColor)
QApplication.setStyle(QWindowsStyle())
a=QApplication(sys.argv)
themes=Themes()
themes.setCaption(Theme (QStyle) example)
themes.resize(640,400)
a.setMainWidget(themes)
themes.show()
return a.exec_loop()
if __name__=="__main__":
main(sys.argv)
As you can see, its a lot of work to create a style from scratch, and in this case, the
result is not very impressive, but very retro, especially if we also use the classic
Chicago font:
519
Chapter 22. Gui Design in the Baroque Age
520
Chapter 23. Drag and drop
PyQt fully supports standard drag and drop operations on all platforms. This
includes both Windows OLE drag and drop, and the two X11 standards: XDND
(which uses MIME) and the legacy Motif DragnDrop protocol.
MIME, which you may know as a way to encode all kinds of datatypes for e-mail,
is used to encode the dragged data in Qt. This is a very flexible standard, regulated
by IANA (http://www.isi.edu/in-notes/iana/assignments/media-types/). This means
that almost any kind of data can be handled by the Qt drag and drop mechanism, not
just text or images.
"""
kalamview.py - the editor view component for Kalam
521
Chapter 23. Drag and drop
import kalamconfig
from resources import TRUE, FALSE
class KalamMultiLineEdit(QMultiLineEdit):
How does this bit of code work? First, you can see that the custom widget accepts
drop events: self.setAcceptDrops(TRUE).
The dragEnterEvent() method is fired whenever something is dragged over the
widget. In this function we can determine whether the object that is dragged over
our application is something wed like to accept. If the function
QTextDrag.canDecode() returns true, then we know we can get some text out of
the data the drop event carries. We accept the event, which means that whenever the
cursor enters the widget, the shape will change to indicate that the contents can be
dropped.
If the user releases the mouse button, the function dropEvent() is called. This
presents us with a QDropEvent object, which we can decode to get the contents.
However, here we come across one of the inconsistencies of the PyQt bindings to
Qt. Sometimes a function wants to return two values: a boolean to indicate whether
522
Chapter 23. Drag and drop
the operation was successful, and the result of the operation. In the case of
QFontDialog, the results are returned in a tuple:
class MyIconView(QIconView):
def dragObject(self):
return QTextDrag(self.currentItem().text(), self)
523
Chapter 23. Drag and drop
Once you have determined what should start a drag (and this can be anything,
really), starting a drag operation is a simple matter of creating a new
QDragObject. This could be, for instance, a QTextDrag or a QImageDrag. Take
care that you keep a reference to the drag objectif you delete it, or allow it to go
out of scope, it will disappear.
In the next example, simply moving a mouse will start a drag operation:
#!/usr/bin/env python
class DropDemo(QListView):
def __init__(self,parent=None,name=None):
QListView.__init__(self,parent,name)
self.setAcceptDrops(1)
self.setGeometry(10,10,100,60)
self.addColumn("Column 1")
self.i = QListViewItem(self, "Hello")
self.ii = QListViewItem(self, "Hello 2")
r = self.itemRect(self.i)
r = self.itemRect(self.ii)
def startDrag(self):
524
Chapter 23. Drag and drop
if __name__ == __main__:
a = QApplication(argv)
w = DropDemo()
w.setCaption("Drag and Drop Test")
w.resize(120,80)
a.setMainWidget(w)
w.show()
a.exec_loop()
23.3. Conclusion
Having done all this work, it is probably discouraging to note that drag and drop has
already been completely integrated into the QMultiLineEdit class. This means
that our work was quite superfluous, though hopefully instructive.
I have only touched upon the fringes of this subject. If you want to create your own
datatypes in order to drag and drop complex objects (for instance animation cells, or
three-dimensional CAD drawings, or perhaps complete Python object hierarchies),
then you must implement new Mime types. This goes way beyond the scope of this
book; I suggest you first read the Qt online documentation on the subject, and then
start experimenting.
525
Chapter 23. Drag and drop
526
Chapter 24. Printing
Printing: a subject that strikes fear into the heart of even the most stalwart
developerand with good reason. Getting a users application data neatly on paper
demands a lot of skill.
Common problems include different printer capabilities, differences between fonts
on screen and on the printer, differing paper sizes, and platform differences in
getting the output to the printer.
PyQt brings a measure of relief to some of these problems, though not to all. It
provides you with the QPrinter object, which is a type of QPaintDevice that has
a built-in common dialog for printer settings. This takes care of most problems
associated with font handling and printer capabilities. With Qt 3 you can even send
TrueType fonts to the printer. The hard workthe layout of the page for paperis
still your job.
Printing on Unix/X11 requires creating PostScript files and sending them to lpr. On
Windows systems you can use the built-in printer drivers. On a Unix system you
can always chicken out; if your application data is plain text or source code, you can
spool it directly to the printer, or via a pretty-printing program such as a2ps.
For the moment, we will not pursue that option, and instead use QPrinter to add
printing capabilities to Kalam.
# kalamapp.py
class KalamApp(QMainWindow):
"""KalamApp is the toplevel application win-
dow of the kalam unicode editor
application.
527
Chapter 24. Printing
"""
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
...
# Create the printer object
self.printer = QPrinter()
...
QPrinter is more configurable than most QPaintDevices. You can set the
printer, printer driver, paper size, number of copies to be printed, and so on. You can
set those configuration options programmatically, but some of them can also be
changed by the user. If you call the setup() function on the printer object, a
printer setup dialog will popup:
528
Chapter 24. Printing
of the application. You might want to save the printer settings to the configuration
file (I havent done that here).
def initActions(self):
self.actions = {}
...
self.actions["filePrint"] = QAction("Print",
QIconSet(QPixmap(fileprint)),
"&Print",
QAccel.stringToKey("CTRL+P"),
self)
self.connect(self.actions["filePrint"],
SIGNAL("activated()"),
self.slotFilePrint)
...
def initMenuBar(self):
self.fileMenu = QPopupMenu()
...
self.actions["filePrint"].addTo(self.fileMenu)
...
self.menuBar().insertItem("&File", self.fileMenu)
...
def initToolBar(self):
self.fileToolbar = QToolBar(self, "file operations")
...
self.actions["filePrint"].addTo(self.fileToolbar)
...
...
529
Chapter 24. Printing
def slotFilePrint(self):
if self.printer.setup():
print "Printing"
...
The printer setup dialog is shown whenever the slotFilePrint method is called.
Printing will commence when the user presses "OK."
def slotFilePrint(self):
margin = 10
pageNo = 1
if self.printer.setup(self):
self.statusBar().message(Printing...)
view = self.workspace.activeWindow()
p = QPainter(self.printer)
p.setFont(QFont("courier", 10))
y = 0
fm = p.fontMetrics()
metrics = QPaintDeviceMetrics(self.printer)
for i in range(view.numLines()):
if margin + y > metrics.height() - margin:
pageNo = pageNo + 1
self.printer.newPage()
y = 0
530
Chapter 24. Printing
p.drawText(margin,
margin + y,
metrics.width(),
fm.lineSpacing(),
Qt.ExpandTabs | Qt.DontClip,
view.textLine(i))
y = y + fm.lineSpacing()
self.statusBar().message(Printing completed,2000)
else:
self.statusBar().message(Printing aborted,2000)
You can see how printing text works. A QPrinter object is a paint device, so we
create a QPainter for it.
Printing requires choosing a font. In all probability, the users screen font is not
suitable for printing. For instance, many people read text on screen in 12 or 14 point
fonts, but prefer printing with 10 point fonts. In the preceding code, a ten-point
courier is chosen, though ideally you would want the choice of printing font to be
part of the application settings.
Once the font is set, we can use QPainter.fontMetrics() to retrieve the height
that each line will take on the paper. If the top margin (margin) plus the current
line position (y) is greater than the height of the page, its time for a new page. The
page height is retrieved with metrics.height() which uses
QPaintDeviceMetrics to provide this kind of practical information.
Actually printing each line is no different from painting text with a QPainter on a
QPaintDevice. The drawText() paints the text on the device. You have to
compute the x and y position, width and height of the area covered by the text to
determine where exactly the text is printed.
These are Qt.AlignmentFlags , so you can mix and match AlignLeft,
AlignRight, AlignHCenter, AlignTop, AlignBottom, AlignVCenter,
AlignCenter, SingleLine, DontClip, ExpandTabs, ShowPrefix,
WordBreak. In this case, ExpandTabs is used to make sure any tabs in the text are
neatly printed, and DontClip is used to prevent errors when a line is too long for
the page.
531
Chapter 24. Printing
24.4. Conclusion
This concludes a brief look at printing with Python and PyQt. In summary, anything
you are clever enough to paint yourself on screen can be painted on paper. PyQt
delivers you from driver hell, and can help you with issues like printing
resolution but youre still responsible for a lot of the work yourself.
This also concludes the development of Kalam. In the next chapter we
internationalize the interface, and in the final we chapter create installable packages
so the world can replace their favorite editor with our powerful alternative!
532
Chapter 25. Internationalizing an
Application
For more than a century people have been uttering the platitude that the world is
getting smaller all the time. Thats nonsense: its getting bigger. Although most
computer users are still able to work with English-only applications, even speakers
of really obscure languages, like Limbu, own computers and would like some
applications in their own language.
An open-source effort like KDE offers more-or-less complete translations of the
entire desktop, including all applications in dozens of languages. And, for a
consideration, you can get a version of Windows in your own language, too, even if
that language is Basque.
Of course, there are other aspects to the internationalization of an application, like
date and number formats, currency, keyboard, preferred dialog layout and so on.
Some of these aspects are handled by Qt - like reversing the dialog layout if the
current script is right-to-left. Others, like the date and number formats are handled
by Pythons locale module - which is alas severely underdocumented.
Translating texts on screen can be handled either by PyQt - using the QTranslator
class, or by Python itself - using the gettext module. PyQts QTranslator is far
more convenient in use, but gettext is based on the wide-spread GNU gettext
library, which is also used by KDE for its translations.
533
Chapter 25. Internationalizing an Application
self.actions["fileNew"] = \
QAction(self.tr("New"),
QIconSet(QPixmap(filenew)),
self.tr("&New"),
QAccel.stringToKey(self.tr("CTRL+N",
"File|New"))
self)
self.connect(self.actions["fileNew"],
SIGNAL("activated()"),
self.slotFileNew)
self.actions["fileOpen"] = \
QAction(self.tr("Open"),
QIconSet(QPixmap(fileopen)),
self.tr("&Open"),
QAccel.stringToKey(self.tr("CTRL+O",
"File|Open")),
self)
self.connect(self.actions["fileOpen"],
SIGNAL("activated()"),
self.slotFileOpen)
...
You must not only mark all text that will appear on screen, but also all accelerator
keys, otherwise translators wont be able to translate them. The extra argument to
tr() gives the translator some extra context.
The tr() serves two purposes: at first, it used as a recognition point for a small
utility that extracts the strings to create message catalogs - files full of translatable
text that you can send your Limbu speaking friends to translate for you.
Secondly, when you run the application, tr() looks in a message database to find
the right string. This is a very fast operation, so you dont have to worry about
performance loss.
After youve marked all translatable strings, you can use a utility to generate
translatable message files. Qts utilityeither lupdate or findtrcan only work
with strings marked with tr(), and only with double-quoted strings.
534
Chapter 25. Internationalizing an Application
Bug warning
There is a significant, though quite esoteric, difference
between the way Qt2 and Qt3 handle the tr(). This means
that when you use a version of PyQt designed to work with Qt
2, the tr() doesnt work out of the box. You need to add a
tr() to all your classes that calls qApp.translate(). This is
what is done in the current Kalam code, because I wrote and
developed the book using PyQt 2.5.
Another important difference: in Qt 3, you can also use
trUtf8() , if the source text is in the utf-8 encoding. That
means that if your translators produce utf-8 encoded files,
instead of plain two-byte Unicode text, you should use this
function, instead of tr(). With PyQt 3 for Qt 3, trUtf8*() will
be used automatically by pyuic.
You can also tell pyuic to use another function instead of tr() - for instance, the
Python pygettext.py default _(). If you do that, with the command:
there will be one important difference: by default, the translation function tr() has
class-local scope, i.e. it is prefixed with self. But a custom translation function has
global scope - exactly what you need for the Python implementation of gettext.
So, you can either do:
The resulting files are almost identical - except for the minor detail of order. You
should make a copy of these files for every language you need a translation for, and
send them to your translators. They can use any editor, or a specialised application
535
Chapter 25. Internationalizing an Application
like KBabel to translate the text, and send it back in the form of a translated .pot
file.
KBabel
The result can be compiled to .mo files using the msgfmt.py utility which should
hide somewhere in you Python installation.
Finally, you can use these message catalog by loading it and installing a global
function _(). (That should have been the function you used to mark your strings):
import gettext
gettext.install(kalam)
import gettext
gettext.install(kalam, /usr/share/locale, unicode=1)
Here, the path should point to a locale directory where all message files can be
found.
536
Chapter 25. Internationalizing an Application
If you are working with Qt 3.0, you can also use a new tool: Qt Linguist. This
extracts the messages to a special, xml-based, format, and you can create message
catalogs with a nice GUI frontend.
To use Qt Linguist, you need to make a Qt project file containing the following text:
SOURCES = configtest.py \
dlgfindreplace.py \
dlgsettings.py \
docmanager.py \
docmanagertest.py \
edmund.py \
frmfindreplace.py \
frmsettings.py \
kalamapp.py \
kalamconfig.py \
kalamdoc.py \
kalamview.py \
macromanager.py \
macromanagertest.py \
main.py \
resources.py \
sitecustomize.py \
startup.py
TRANSLATIONS = kalam_nl.ts
After spewing out a lot of warnings (this tool expects C++, not python) a file in xml
format is created which you can edit with an editor or with Qt Linguist.
537
Chapter 25. Internationalizing an Application
If the translator is finished, he or she can choose "release" in the menubar and
generate a .qm message catalog.
Using this catalog in your application is a simple matter of installing the appropriate
translator:
#!/usr/bin/env python
"""
main.py - application starter
from qt import *
538
Chapter 25. Internationalizing an Application
def main(args):
app=QApplication(args)
translator = QTranslator(app)
translator.load("kalam_" + locale.getlocale()[0] + ".qm",
kalamconfig.get("libdir","."))
app.installTranslator(translator)
kalam = KalamApp()
app.setMainWidget(kalam)
kalam.show()
if len(args) > 1:
for arg in args[1:]:
document=KalamDoc()
document.open(arg)
kalam.docManager.addDocument(document, KalamView)
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
Two remarks: note how we use the locale module to determine the language of
the user. This returns a tuple containing a language code and a character set that
correspond the user locale, as set by the operating system: [en_US,
ISO8859-1]. If you always use the language code as the second part for your
filename, then Qt will be able to determine which translation file to load.
Note also that the location of that message file is determined by a configuration
option. Standard Unix .mo files tend to go into /usr/share/locale/ , but there is
no corresponding standard for Qt .qm messages, and you might as well put those in
the application installation directory. Where that is, will be determined in the next
chapter.
539
Chapter 25. Internationalizing an Application
540
Chapter 26. Delivering your
Application
26.1. Introduction
Packaging your software for installation is a difficult, nasty, unpleasant, arduous,
error-prone task. It is awfully enticing to just give up, zip up your Python source
code together with a README file, and leave it at that.
In some cases, doing just that might be wise: if your intended users are technically
knowledgeable, you can ask them to install Python, edit system variables, and mess
around until everything works. Typically, though, more than this is expected.
The first problem of packaging an application for installation arises because of the
wide variety of platforms a PyQt application will run on: Classic Unix, Linux, the
free BSDs, Windows in its infinite variety and finally OS X. Depending upon your
target audience, one or more of these platforms can be dropped. If your application
is open source, you might be able to get other developers to package your
application for their platform.
The second problem is that Python has several methods of packaging applications.
The standard is Distutils, which comes with the Python distribution. Then there is
freeze, Gordon McMillans Installer, Fredrik Lundhs Squeeze (which is packaged
with the PythonWorks IDE), and finally Thomas Hellers py2exe (which makes use
of Distutils). There are also generic commercial solutions, such as Wise or
InstallShield (both for Windows) and InstallAnywhere (for all platforms that
support Java). Furthermore, there are free alternatives, such as rpm or dpgk for
Unix. This breadth of choice alone points to the fact that creating installation
packages is a difficult problem that has yet to be solved.
Distutils is the standard Python solution and comes with Python 2.x. It appears to be
more geared to distribution modules and libraries, and less to distributing
applications. If you want something that generates stand-alone executables of an
application, you might want to try Gordon McMillans Installer
(http://www.mcmillan-inc.com/builder.html). BlackAdder will probably provide an
installation utility in a future version, and it will probably be based on Distutils.
541
Chapter 26. Delivering your Application
The third problem (they do mount up) is that you cannot assume that your user has
Python installed. You must choose whether you want your users to install Python
themselves, or package a complete Python installation with your application. The
first option is perfectly feasible on Linux, because installing Python using either
rpm or apt-get is easy enough. The second option might be feasible on Windows, as
Python for windows comes with a very nice and easy installer. Of course, Windows
users are generally a bit lazier than Unix users, and might not want to install another
package before they can start using your application.
The fourth problem is the presence, or absence, of PyQt. Again, most modern Linux
distributions include PyQt, so users can just grab the rpm or deb package, and go.
As for Windows, you can freely redistribute the runtime components that come with
BlackAdder, if you have bought the professional version or the non-commercial
PyQt and Qt libraries.
The fifth problem arises if you have used third-party modules that require separate
compilation for each platform, and separate installation.
A sixth problem arises if you have written extensions in C or C++ as part of your
application or library, and want to distribute those, too.
Finally, its difficult to achieve even a little integration with the users desktop. All
user interface platforms Qt supports - Windows, KDE, Gnome, CDE, OS X and
others have wildly different standards for menu options, desktop icons, mime-type
integration (for those open file with application menus). This is, perhaps, the
hardest, as it requires knowledge of all relevant desktop environments.
This chapter will cover the creation of source packages, Windows installers and
Unix rpms using the standard Distutils package. This requires that the user has
already installed Python, PyQt, and any other libraries. The Redhat Package
Manager (rpm) on Linux can be told to check for these dependencies. On Windows,
its a matter of forcing your users to read the manual. I dont describe the process of
packaging your own C or C++ extensions, though it is possible. Consult the
Distutils manual for more information.
542
Chapter 26. Delivering your Application
create a nice package structure. You can zip the source-code up or tar it down, and
deliver it. Youd package the code with a clear README file that details what
settings should be altered in either the .profile (or .bashrc), or, for Windows,
to add a variable to the environment using the friendly dialog window Control Panel
provides.
One of those settings will probably be the PYTHONPATH variable. If the
application is divided into several modules (as Kalam is), the PYTHONPATH must
include the top-level directory where the source is installed. It is not a good idea to
install application source into the Python modules directory.
Installation
Requirements:
543
Chapter 26. Delivering your Application
www.thekompany.com.
Boudewijn Rempt
boud@rempt.xs4all.nl
This is the easiest way out for you as a developer. You can also use Pythons
distutils to create a source distribution of your application. Since this is the first step
to a binary distribution, its a good idea to use distutils, even for source-only
distributions.
26.3.1. setup.py
#!/usr/bin/env python
setup(name = "kalam",
version = "1.0",
description = "Kalam - the extensible Python editor",
author = "Boudewijn Rempt",
author_email = "boud@rempt.xs4all.nl",
544
Chapter 26. Delivering your Application
url = "http://www.valdyas.org",
packages = ["charmap",
"kalamlib",
"typometer",
"workspace",
""],
data_files = [("kalam/data", ["data/Blocks.txt"]),
("kalam/pixmaps", ["pixmaps/listspace.png",
"pixmaps/splitspace.png",
"pixmaps/stackspace.png",
"pixmaps/tabmanager.png",
"pixmaps/workspace.png"])],
scripts = ["kalam","kalam.bat"],
long_description = """
Kalam is a plain-text editor. It is written in Python using
the PyQt GUI toolkit as an example and tutorial for the book
GUI programming with Python and Qt, published by Opendocs.
"""
)
The setup.py is the place to specify all executable parts of your application, and
some metadata. Lets examine all parts:
545
Chapter 26. Delivering your Application
data_files: this is a list of files that are not executable code. These files will be
installed in a default place, like /usr/share on Linux. You must also include all
these files in MANIFEST.in, otherwise they wont be packaged.
scripts: this is a list of python script files. If you use #!/usr/bin/python as the
first line of a script to make it executable on Unix, Distutils will change that to
the location of Python on the users machine.
long_description : a longer description of the application. This is used when
you create an rpm package.
There are other options more concerned with distributing C or C++ extension
modules you have created. I dont cover them here.
Finally, a word of warning: if you are experimenting with setup.py, you will
notice that a file called MANIFEST has been created. Always remove this file after
creating a distribution. It is a kind of cache that lists the set of files that should be
included; if you change this set, distutils will still read MANIFEST instead of your
changes in setup.py.
26.3.2. MANIFEST.in
Despite the data_files option to setup(), it is still necessary to provide a
second file that contains a list of extra, non-Python files that need to be distributed.
This file is called MANIFEST.in (mind the capitalization), and employs its own set
of keywords to specify files to include or exclude.
include kalam
include kalam.bat
recursive-include data *
recursive-include pixmaps *
recursive-include dialogs *
Here, we include the kalam starter script and the kalam.bat and batch file. Then
we recursively include everything in the directories data, pixmaps and dialogs.
546
Chapter 26. Delivering your Application
(The latter is not absolutely necessary for running the application, but it cant hurt
to give people access to our dialog designs.)
The options available for MANIFEST.in are:
26.3.3. setup.cfg
The setup.py script takes myriad command-line options. You can also create a
setup.cfg file that contains the most important options. Amongst those options
are a number that tell the installer to install the application in a specific place. The
user might need to edit these to reflect his preferences. For Unix, a good default is:
[install]
install_lib=/usr/local/share/kalam
install_data=/usr/local/share/kalam
install_scripts=/usr/local/bin
547
Chapter 26. Delivering your Application
The distutils will then spew a lot of text on the screen, and deliver your package in a
subdirectory named dist:
548
Chapter 26. Delivering your Application
549
Chapter 26. Delivering your Application
Thats ita nice, clean and complete source distribution of Kalam. You can
generate both zip archives and gzipped tarballs by providing options on the
command line:
The options are zip, gztar, bztar, ztar and tar, for zipfiles, gzipped tarfiles, bzipped
tarfiles, compressed tarfiles and plain tar files.
boudewijn@maldar:~/doc/pyqt/ch18/kalam/dist/kalam-
1.0 > python setup.py install
Distutils will copy everything to the location designated in setup.cfg, and kalam
will be ready to run!.
550
Chapter 26. Delivering your Application
To ensure a really nice RPM, a few options should be set. The best place to set these
is in the setup.cfg file:
[bdist_rpm]
release = 1
packager = Boudewijn Rempt <boud@rempt.xs4all.nl>
doc_files = README
COPYING
provides = kalam
requires = python pyqt
distribution_name = SuSE 7.2
Most of the options merely provide some extra meta-data, but the provides and
requires options are needed by RPM to do its dependency checking. In this case,
Kalam requires both Python and PyQt to be present, which is hardly a surprise! You
can make these requirements more specific by also asking for a version number:
RPM is a complicated package format in itself, and you can customize the
installation process considerably by using .spec file. The book Maximum RPM
(freely available at http://www.rpmdp.org/rpmbook/) gives detailed information on
writing these files. At the moment distutils writes the file for you, based on the
options in setup.py and setup.cfg. A future release of distutils will support
using your own .spec files.
551
Chapter 26. Delivering your Application
552
Chapter 27. Envoi
And thats it apart from the appendices. There is a lot more I would like to write
about, for example, a chapter on using the mxODBC database driver together with
Qts QTable. I would have liked a chapter dedicated to QEvent and QEventLoop.
More discussion of Qts great QTextEdit text editor, would have been nice (but I
had already started on a Python implementation of an editor widget with
QScrollView). A small example game or an editor implemented entirely in Python
would be fun. Oh, and the Qt OpenGL module has been wrapped too. Wait! I dont
want to stop writingthe subject is large, and there is something new to discover
every day... Well, all I can say, since I have to stop now, is: have fun, and meet me at
the webforum Opendocs provides for this book, comp.lang.python, or on the
PyKDE mailing list.
553
Chapter 27. Envoi
554
IV. Appendices
Table of Contents
A. Reading the Qt Documentation .....................................................................557
B. PyQwt: Python Bindings for Qwt ..................................................................563
C. First Steps with Sip .........................................................................................573
555
Chapter 27. Envoi
556
Appendix A. Reading the Qt
Documentation
Qt is originally a C++ GUI toolkit, and PyQt is just a wrapper around it. Fortunately,
Qt is very well designed and makes full use of the object-oriented qualities of C++,
so the translation is very comfortable, and PyQt feel like a real Python library.
BlackAdder includes a copy of the Qt class documentation (nineteen megabytes of
html text) that has been fully translated to Python. Of course, if you want to use a
more recent version of PyQt than that which comes with BlackAdder, or if you use
PyQt on itself, then you need to read the C++ documentation for all the details that I
didnt have space to discuss in this book. After all, this book teaches you how to use
the toolkit to create complete applications, and isnt a mere duplication of the class
documentation.
Fortunately, reading C++ documentation for use from Python isnt very difficult. In
fact, the translation to Python idiom that is included with BlackAdder has been
achieved for the greater part with a few find & replace scripts. However, if your
knowledge of C++ (or C) is limited to knowing that it exists, then you might want to
read this appendix for some guidance.
557
Appendix A. Reading the Qt Documentation
Lets take a simple Qt class as an example: QLabel. Its a good idea to open the Qt
class documentation in your browser window (remember the KDE shortcut) and
keep that in view.
First, the documentation tells you that the QLabel class includes qlabel.h:
#include <qlabel.h>. This means about the same as a Python import statement. You
can disregard it.
The methods that do not return anything, but have the same name as the class, are
the C++ constructors. Simply call them with the right arguments. Dont pay
558
Appendix A. Reading the Qt Documentation
attention to any spurious asterisks (*) or ampersands (&) around the arguments to
the function: what matters is the type, like QWidget. Dont pay attention to the
const keyword either.
If there is an equals sign (=) after the variable name, then the function can use a
default parameter, just like in Python. Again, just like in Python, booleans are zero
or one. However, a default argument of zero, has a default argument of None in
Python. This is important if you want to use two out of three arguments: then you
must also mention the middle man (you can safely drop any tailing default
arguments you dont need):
versus
label=QLabel("text")
Public members are instance methods of objects. If you call a public member, you
should always prefix the call with the name of the object you have created (or self
if you are calling the method from within the object). For example:
print label.text()
Slots are in no way different from ordinary functions in Python, so what holds for
public members also holds for public slots. Protected member variables are a vague
kind of privateif you create the QLabel from Python, you can access the
protected members, like drawContents(), without problems, but if the QLabel
has been created from a C++ class then you cannot access the protected member
functions.
Properties are currently not supported by PyQteverything you can set and get
with properties is also accessible by get() and set() methods.
If you are reading the detailed description of a class, you will often come across
snippets of C++ code. These are easy to translate, too. Just keep in mind that both a
double semi-colon (::) or an arrow (->) translate to a Python dot (.). And you dont
need braces or final semicolons, of course. Or new statements. For instance:
559
Appendix A. Reading the Qt Documentation
Note also that certain pre-defined values, called constants in C++ (and lots of other
languages), are placed either in a certain class, not object instances or in the Qt
pseudoclass. Thus, the Panel or Sunken constants are accessed from the QFrame
class, while the AlignBottom and AlignRight constants are taken from the Qt
pseudoclass. Note also that it isnt necessary to prefix Qt in C++, but that this is
obligatory in Python.
A bit like constants are static methods, and are defined on the class:
QObject.connect()
QLabel doesnt have any signals or static members. For those we had better look at
another class: QScrollbar.
Signals have already been discussed in detail in Chapter 7. Here I only want to
mention the way you must remove any fluff from the declaration. Signals are placed
in a Python dictionary by sip, so you really want to get the string argument to
SIGNAL() right.
So, if there are no arguments to the signal, you can just copy it, including the
brackets. If there are arguments you need to copy the entire argument list, but not
the variable name. So:
560
Appendix A. Reading the Qt Documentation
QObject.connect(sbar,SIGNAL("valueChanged(int)"),someFunction)
On the other hand, if there are asterisks involved, then you have to copy those, too.
In QListView,
Becomes:
self.connect(self,
SIGNAL("returnPressed(QListViewItem *)"),
self.slotItemSelected)
or:
listview = QListView()
You can also use actual arguments, of course. You almost never need to actually
pass something for the name parameter, but it makes for nicer debugging:
561
Appendix A. Reading the Qt Documentation
As youve seen, its not difficult at all to translate from C++ to Python even if
you dont know any C or C++. If you do want to know more about C++, I can
recommend Steven Ouallines book, Practical C++ Programming as a good
beginners title.
562
Appendix B. PyQwt: Python Bindings
for Qwt
Using sip, it is possible to wrap any C++ library. Jim Bublitz, for instance, has
wrapped the core libraries of KDE 2, and Gerard Vermeulen has wrapped the Qwt
toolkit. This appendix has been written by Gerard Vermeulen to introduce this
extension library.
PyQwt is a set of Python bindings for the Qt Widgets for Technics toolkit, which is
freely downloadable at http://qwt.sourceforge.net. PyQwt is equally free, and
available from http://gerard.vermeulen.free.fr.
Behind the innocuous trigram Qwt a complex set of widgets is hiding. This
extension library, written by Josef Wilgen with the aid of many others, fills in a
noticeable gap in the Qt library: data visualisation. This toolkit features fast plotting
of Numerical Python arrays (and Python lists or tuples) of Python floats.
Remember how we created a rolling chart in Chapter 21 when we investigated
QPainters? It was quite an interesting job, but for serious applications youd need
a stronger package.
Fortunately, Python possesses a very strong array manipulation package: the
Numerical Python Extensions (or, affectionately, numpy, available at
http://www.pfdubois.com/numpy), which, when paired with the Qwt extensions,
gives you the power to create complex graphing and charting applications.
B.1. NumPy
The Numerical Python Extensions, also called NumPy or Numeric, turn Python into
an ideal tool for experimental numerical and scientific computing (better than
specialized programs like MatLab, Octave, RLab or SciLab). NumPy is useful for
everybody who analyzes data with the help of a spreadsheet program like Microsoft
Excelit is not just for mathematicians and scientists who crunch lots of data.
NumPy defines a new data type, NumPy array, and a very complete set of
operators and functions to manipulate NumPy arrays. All the functionality of
NumPy can be obtained in pure Python, but NumPy gives you speed and elegance.
563
Appendix B. PyQwt: Python Bindings for Qwt
In the following, I assume that you have installed NumPy on your system. Doing so
is not really difficult. There are binary packages for Windows, or source packages
for all platforms. A source package is installed using distutils (see Chapter 26), by
typing
Once numpy is installed, you can start Python (or open the Interpreter window in
BlackAdder) and import the NumPy extension:
A NumPy array looks like a list and can be created from a list (in fact, from any
sequency type: list, tuple or string).
Lets create and print a 1-dimensional NumPy array of Python floats:
>>> a = ar-
ray([1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0])
>>> print a
[ 1. 4. 9. 16. 25. 36. 49. 64. 81. 100.]
>>>
This creates a 1-dimensional NumPy array. All elements in the list should have the
same data type.
A 2-dimensional NumPy array is created from a list of sub-lists:
564
Appendix B. PyQwt: Python Bindings for Qwt
The sub-lists should have the same length, and all the elements in all the sub-lists
should have the same data type.
You can show off with NumPy arrays of even higher dimensions (up to 40, by
default). For example, a 3-dimensional NumPy array is created from a list of
sub-lists of sub-sub-lists:
>>> c = ar-
ray([[[0.0, 1.0], [2.0, 3.0]], [[4.0, 5.0], [6.0, 7.0]]])
>>> print c
[[[ 0. 1.]
[ 2. 3.]]
[[ 4. 5.]
[ 6. 7.]]]
>>>
The sub-lists should have the same length, the sub-sub-lists should have the same
length, and all elements of all sub-sub-lists should have the same data type.
In the following, I am going to compare the functionality of NumPy arrays and
lists. Here is an easier method to create a NumPy array:
The function call arange(0.0, 5.0, 0.5) returns an array with elements
ranging from 0.0 to 5.0 (non-inclusive) in steps of 0.5. Here is a similiar function to
return a list with the same properties:
565
Appendix B. PyQwt: Python Bindings for Qwt
After copying and pasting the function definition in your Python interpreter, do:
Why are NumPy arrays better than lists? The full answer is speed and elegance.
To compare lists and NumPy arrays with respect to elegance, lets use a simple
function:
def lorentzian(x):
return 1.0/(1.0+(x-2.5)**2)
To calculate a list, ly, containing the function values for each element of ly, we can
do:
>>> ly = [0.0]*len(lx)
>>> for i in range(len(lx)): ly[i] = lorentzian(lx[i])
...
Do you know that you can get rid of the loop? The following is more elegant and
slightly faster:
>>> ay = lorentzian(ax)
Almost magic, isnt it? I wrote the function lorentzian(x) assuming that x is Python
float. If you call lorentzian with a NumPy array as argument, it returns a NumPy
array. This does not work with lists:
>>> ly = lorentzian(lx)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
566
Appendix B. PyQwt: Python Bindings for Qwt
To compare speed, we create a list, xl, and a NumPy array, xa, with 100000
elements and use the profile module to time the statements yl =
map(lorentzian, xl) and ya = lorentzian(xa) :
>>>
567
Appendix B. PyQwt: Python Bindings for Qwt
On my computer, the Numerical Python extensions are almost 25 times faster than
pure Python!
There exists a scientific plotting program, SciGraphica
(http://scigraphica.sourceforge.net), which allows you to manipulate your data in a
spreadsheet. The underlying engine is a python interpreter with the NumPy. Each
column in the spreadsheet is in reality a NumPy array. This clearly demostrates
the power of this extension. If you want to know more about NumPy, you can
consult the excellent documentation at http://www.pfdubois.com/numpy, the
homepage of NumPy.
B.2. PyQwt
Qwt and PyQwt exists for both Linux/Unix and Windows. Qwt is a set of plotting
widgets. Installing these is currently not quite as comfortable as installing numpy,
but the instructions in the package are excellent:
568
Appendix B. PyQwt: Python Bindings for Qwt
root@calcifer:/home/boud/src/qwt-
0.3.0 > mkdir /usr/local/include/qwt
This did the trickat least on my system! Now you just have to instal the Python
bindings. This is even easier, since PyQwt now uses Distutils to get itself installed.
However, note that you need to have a source installation of PyQt if you intend to
build PyQwt from source. There are currently binary packages for Windows and
some versions of Linux, like Mandrake.
PyQwt has a number of illustrative demo scriptshere, I picked one to demonstrate
to you the way it works.
#!/usr/bin/env python
#
# qwtdemo.py
#
# Demonstrates that you can plot NumPy ar-
rays and lists of Python floats.
# NumPy arrays are more ele-
gant and more than 20 times faster than lists.
import sys
from qt import *
from qwt import *
from Numeric import *
def lorentzian(x):
return 1.0/(1.0+(x-5.0)**2)
569
Appendix B. PyQwt: Python Bindings for Qwt
class ListArrayDemo(QWidget):
def __init__(self, *args):
apply(QWidget.__init__, (self,) + args)
# admire
app = QApplication(sys.argv)
demo = ListArrayDemo()
app.setMainWidget(demo)
demo.resize(400, 600)
demo.show()
app.exec_loop()
570
Appendix B. PyQwt: Python Bindings for Qwt
Output of qwtdemo.py
As you can see, the core of the Qwt library is the QwtPlot widget - an object that
knows how to plot, but can be used as any other Qt widget.
571
Appendix B. PyQwt: Python Bindings for Qwt
572
Appendix C. First Steps with Sip
Jim Bublitz knows far more about sip, the tool used to wrap C++ libraries for
Python, than I do. For instance, hes the author of the bindings to the KDE2
libraries. For these reasons, I asked him to write an appendix on using sip.
C.1. Introduction
Wrapping C++ libraries for use from Python is a profitable undertaking, given the
wealth of functionality that becomes available after doing so. Python, in contrast
with Java, has been designed from the outset to be easily extensible with foreign
libraries written in C. C++, however, is a far more complex language than C, and
requires careful attention to the creation, ownership and destruction of objects.
Trolltech have made the work even more complicated with the invention of a special
meta-object pre-compiler needed to get techniques like signals and slots working.
These days it is not usual to wrap C++ libraries by hand. If only because of the size
of the Qt libraries, it probably isnt practical to write these bindings manually, so an
automated tool, sip, was developed by Phil Thompson (the PyQt developer) to
generate the necessary binding code.
As you know, PyQt is a set of Python bindings for the Qt libraries. That means that
PyQt isnt a translation of Qt into Pythoninstead, the bindings let you access
and use the C++ Qt libraries from the Python language via an intermediate wrapper
library, which is also written (or rather, generated) in C++.
The sip program generates these C++ wrappers. Wrappers are chunks of C++
code that allow Python to pass data to and from other C++ code and to invoke C++
methods or functions. Sip gets its name (and some of its architecture) from another
wrapper generator named swig. In fact, sip started out as a small swig, although it
has grown a bit since then. It is specifically designed to generate Python bindings
for C++ libraries, while swig is more general purpose, and can wrap C libraries for a
variety of scripting languages. Of course, sip also offers some additional
functionality, like support for signals and slots, Python inheritance of C++ objects,
and many other C++ language features:
573
Appendix C. First Steps with Sip
574
Appendix C. First Steps with Sip
point in creating the .sip file. It is a bit of a drudgery, since transforming a header
file into an input file for sip is mostly handwork.
Shown is a fragment of the qmultilinedit.sip file created from
qmultilinedit.h . If you look at the original file in the PyQt sources, you will
find at the top of the complete qmultilinedit.sip some code for generating
documentation, which is omitted here.
public:
QMultiLineEdit(QWidget * /Transfer-
This/ = 0,const char * = 0);
QMultiLineEdit *ptr;
if (sipParseArgs(&sipArgsParsed,sipArgs,
"m",
sipThisObj,sipClass_QMultiLineEdit,
&ptr))
{
int line, col;
return Py_BuildValue("(ii)",line,col);
}
%End
575
Appendix C. First Steps with Sip
Most of the process of creating a .sip file is deleting all of the things SIP doesnt
need or cant use. Typically all comments are stripped from the .h file in creating the
.sip file, since they arent necessary for SIP and are still available in the original .h
file. For PyQt, SIP only uses methods and variables from specific parts of each class:
All private variables are deleted from the C++ header (.h) file, as are all protected
variables. Public methods and variables are retained.
Normally all private methods are also deleted, but there are one or two cases where
they are useful. For example, declaring a private copy constructor prevents SIP from
automatically generating a public copy constructor.
Next, all parameter names are deleted. For instance:
...
void cursorPosition( int *line, int *col ) const;
...
becomes
...
void cursorPosition(int *,int *) const;
...
sip does not understand (or need) method parameter names, and in fact any
parameter names left in the .sip file will cause a sip syntax error when sip is run
on the file. Note also that the public directive is removed from the class
declaration line, as is any Q_OBJECT declaration or any friend class declarations
when these are present. Any inline C++ code is also removed.
576
Appendix C. First Steps with Sip
Call by reference and call by value: C and C++ (and Visual Basic and a
host of other programming languages) have two ways of passing arguments
to functions: by reference, or by value. If a function is called with arguments by
reference, changing the value of the arguments will change their value outside
the function. Python only has call by (object) reference. If an object is mutable,
then changing it inside a function, will also change the object outside a
function:
577
Appendix C. First Steps with Sip
When you can a Python function with an immutable object like a string or an
integer, the value of the reference outside the function wont change:
>>>
>>> def f(a):
... a="b"
...
>>> a="a"
>>> f(a)
>>> a
a
>>>
Thats because the name of the argument and the name of the variable are
not aliases, they are two seperate names, that might point to different objects,
or to the same. As soon as you assign a new string to the argument, the
references no longer point to the same name.
%MemberCode
// The Python interface returns a tuple.
QMultiLineEdit *ptr;
if (sipParseArgs(&sipArgsParsed,sipArgs,
"m",
sipThisObj,
sipClass_QMultiLineEdit,
&ptr))
{
int line, col;
578
Appendix C. First Steps with Sip
return Py_BuildValue("(ii)",line,col);
}
%End
However, sip cant determine whether these pointers contain data being sent to the
cursorPosition function, or data being returned by cursorPosition to the
calling code. Since Python has nothing comparable to a C/C++ pointer, there is no
automatic way to generate the wrapper for this method. Inside knowledge of what
the code actually does is required to generate wrappers, and the developer has to
provide sip with this knowledge.
Immediately following the cursorPosition method declaration is a %MemberCode
declaration. This allows the developer to tell sip how to wrap this function by
providing most of the necessary C++ code. The contents of the %Membercode
block is, in fact, C++ code.
Looking at the %Membercode block, sip is instructed to first parse the arguments
which Python will pass to the wrapper code. The sipParseArgs function is the
most important, and complex, function in the sip library. The third parameter is a
string that encodes the number and types of the Python parameters that are expected,
with more information given in later parameters. In this case the sipParseArgs is
being told to expect exactly one Python parameter which is a Python instance of the
QMultiLineEdit class which corresponds to the value of self.
Next, some variables are defined (line, col) to hold the data cursorPosition
will return. The member next calls the Qt/C++ version of
QMultiLineEdit::cursorPosition , which will fill in the values pointed to by
&line and &col.
Last, the code uses the Py_BuildValue to return the line and col values
obtained to the Python caller. In this case, since two values are returned, the code
places the two values in a tuple which is returned to the Python program that called
the method. Even though a Python method can return 2 or more distinct values,
C++ cant, and the member code is in the C++ domain.
If you look at some of the code sip generates, youll find it looks very much like the
member code generated manually for the cursorPosition method. There will be a
number of references to functions or methods which begin with sip or Py. The
579
Appendix C. First Steps with Sip
Py-prefixed calls are to code built into Python itself. Python contains a library of
functions used to write wrappers/interfaces for C and C++ code. The sip-prefixed
functions are calls to the sip library, and occur in all sip-generated code (including
all of PyQt). This is why you need to have the sip library installed before you can
use PyQt, even though you dont need to run the sip program itself.
580
Appendix C. First Steps with Sip
compile a particular set of .sip files into a module Python can use. Since SIP, like
PyQt, is open source, you can also look at the sip source code itself. The PyKDE
mailing list also includes discussions of sip usage and coding, and is a place where
you can post your questions.
C.6.1. Usage
sip [-h] [-V] [-c dir] [-d file] [-m file] [-I dir] [-s suffix] [-p
module] [file]
where:
581
Appendix C. First Steps with Sip
C.6.1.2. Limitations
The following limitations are SIP compile time options:
C.6.1.3. Files
*.sip
and possibly further included files are processed by SIP. They are the source file(s)
defining the wrapping. They closely resemble the header files they describe but also
contain additional directives for a range of special cases and purposes (e.g.
production of documentation, treatment of different versions a wrapped libraries).
582
Appendix C. First Steps with Sip
$(module).py
is produced by SIP. It initializes the wrapping and imports the dynamic lib
lib$(module)c.pyd which contains the actual C++ wrapping code.
Code from %PrePythonCode and %PythonCode sections is also placed in this file as
in the following example (assuming a module MOD wrapping classes A and B):
class A:
...
class B:
...
libMc.sipRegisterClasses()
# here comes code from %PythonCode sections
lib$(module)c.pyd
This dynamic library is compiled from the following, SIP generate intermediate files:
sip$(module)$(class).h , sip$(module)$(class).cpp
A pair of corresponding header and C++ files for each wrapped class.
583
Appendix C. First Steps with Sip
sip$(module)Decl$(module).h
A global module header, which (beside all im- and exports) contains all
%ExportedHeaderCode and %HeaderCode (from imported .sip files only
%ExportedHeaderCode).
$(module)cmodule.cpp
makefile_name
sip_helper.cpp
All code from %VersionCode sections will be placed in this file. Typically it is
compiled into sip_helper.exe, which then produces sip$(module)Version.h.
This is included in all relevant files to maintain version information for the
conditional sections.
Why not just #include the necessary file that provides the version information and
use the C++ test specified as part of the version definition in the .sip files?
The answer is that moc cant handle C++ pre-processor commands, so the proxy
header file must be run through the C++ pre-processor beforehand. The code
generated by moc is then #included by the main module code.
The net result is that the header file specifying the version information is #included
by the main module code and #included a second time - but the second time is a
584
Appendix C. First Steps with Sip
version that has already been run through the C++ pre-processor and has therefore
lost its usual means of protecting itself from being #included twice.
Unless the file follows certain rules (like having no function definitions) it is likely to
make the C++ compiler complain. Therefore the solution is to use a generated file
that isnt going to cause complaints.
docfile_name
C.6.1.4.2. Macros
A number of macros can be used in the .sip files (e.g. class definitions, makefile
templates). When SIP parses the definitions, the macros are replaced by actual
values as follows:
585
Appendix C. First Steps with Sip
$$ a $ character
$C Class name
$S Source files
$O Object files
$c C++ file suffix
$o Object_file_suffix
$m C++ module name
$P Percent sign
C.7. Directives
Sip has a number of directives that control the way C++ code is generated. Since sip
is essentially completely undocumented, it was impossible to clearly describe all
directives, or even to be sure that this is an exhaustive list. Still, this list come in
useful. The directives are grouped according to function: Documentation, Modules,
Conditional Elements, C++ and Header Code, Python Code sections, Mapped
Classes, Special Python methods and Other.
C.7.1. Documentation
%Copying
Name
%Copying Start of software license _block_
586
Appendix C. First Steps with Sip
Synopsis
%Copying
Remarks
If more than one is given, all blocks are included in the order of evaluation. Copying
blocks are not extracted from imported modules.
%Doc
Name
%Doc Start of a documentation _block_
Synopsis
%Doc
Description
If SIP option -d doc_file is used, these blocks are collected (in the order of
evaluation) from within the main module, but ignored on all imported modules and
put into the doc_file.
587
Appendix C. First Steps with Sip
%ExportedDoc
Name
%ExportedDoc Start of an exported documentation _block_
Synopsis
%ExportedDoc
Description
If SIP option -d doc_file is used, these blocks are collected from all modules
(incl. imported modules) and put into the doc_file.
C.7.2. Modules
%Module
Name
%Module Definition of the (main) modules name
588
Appendix C. First Steps with Sip
Synopsis
%Module module_name
Description
The module_name must be defined at least once. If multiple definitions are given
the last one is used. The produced wrapping consists of the following files:
Bear in mind, that you also need sip.lib and of course a lib with your classes to be
wrapped.
%Include
Name
%Include Include a file
Synopsis
%Include include_file_name
589
Appendix C. First Steps with Sip
Description
Include a file.
%Import
Name
%Import Import module (dynamic library)
Synopsis
%Import import_module_name
Description
The imported .sip file is parsed. %ExportedHeaderCode and %ExportedDoc
sections are extracted. %HeaderCode, %C++Code, %ExposeFunction, %Copying,
%PrePyCode, %PyCode, %Doc and %Makefile blocks are ignored.
The wrapping python file imports the dynamic lib libimport_module_namec
(as .pyd or .dll) before libmodule_namec.
This does not mean that the imported lib is available as a wrapped module as well.
To achieve this, you have to import import_module_name.py too.
590
Appendix C. First Steps with Sip
%If
Name
%If start of a conditional _block_
Synopsis
%If condition
Description
The following (conditional) block is evaluated only if condition is true.
Currently the only valid type of condition is Version() (see below).
%End
Name
%End Terminate _block_
591
Appendix C. First Steps with Sip
Synopsis
%End
Description
All _block_s must be closed by a matching %End directive.
Version()
Name
Version() Condition function for %If directive
Synopsis
Version(version_range);
Parameters
version_range: enclosed in parenthesis one or both of low_Bound and
high_bound separated by a -
592
Appendix C. First Steps with Sip
Remarks
This is the only currently available type of condition for %If.
Examples
%Version
Name
%Version Define version value(s) and evaluation methods
Synopsis
Examples
593
Appendix C. First Steps with Sip
%PrimaryVersions
Name
%PrimaryVersions Define list of primary versions
Synopsis
%PrimaryVersions list_of_primary_version
Remarks
The list must be enclosed by braces.
Examples
594
Appendix C. First Steps with Sip
%VersionCode
Name
%VersionCode Start of a version code _block_
Synopsis
%VersionCode
Description
The %VersionCode goes into sip_helper.cpp, which will be compiled into
sip_helper.exe , which will produce sip$(module)Version.h , which will be
included in (almost) all files. It should contain #defines corresponding to each of the
different versions.
%HeaderCode
Name
%HeaderCode Start of a C++ header code _block_
595
Appendix C. First Steps with Sip
Synopsis
%HeaderCode
Description
Header code is written to the global module header
sip$(module)Decl$(module).h . These sections are collected from the main
module (in the order of evaluation) but ignored on all imported modules.
%ExportedHeaderCode
Name
%ExportedHeaderCode Start of an exported C++ header code _block_
Synopsis
%ExportedHeaderCode
Description
The ExportedHeaderCode is written to the global module header
sip$(module)Decl$(module).h . These sections are collected from all modules
(incl. imported modules).
596
Appendix C. First Steps with Sip
%ExposeFunction
Name
%ExposeFunction
Synopsis
%ExposeFunction
%C++Code
Name
%C++Code Start of a C++ code _block_
Synopsis
%C++Code
597
Appendix C. First Steps with Sip
%MemberCode
Name
%MemberCode Start of a C++ member code _block_
Synopsis
%MemberCode
%VirtualCode
Name
%VirtualCode Start of a C++ virtual code _block_
Synopsis
%VirtualCode
598
Appendix C. First Steps with Sip
%VariableCode
Name
%VariableCode Start of an access code _block_
Synopsis
%VariableCode
Remarks
%VariableCode cannot be specified for non-static class variables.
%PythonCode
Name
%PythonCode Start of a Python code _block_
Synopsis
%PythonCode
599
Appendix C. First Steps with Sip
%PrePythonCode
Name
%PrePythonCode Start of a pre-Python code _block_
Synopsis
%PrePythonCode
600
Appendix C. First Steps with Sip
%ConvertFromClassCode
Name
%ConvertFromClassCode Start of a from-class code _block_
Synopsis
%ConvertFromClassCode
%ConvertToClassCode
Name
%ConvertToClassCode Start of a to-class code _block_
Synopsis
%ConvertToClassCode
601
Appendix C. First Steps with Sip
%CanConvertToClassCode
Name
%CanConvertToClassCode Start of a can-to-class code _block_
Synopsis
%CanConvertToClassCode
%ConvertToSubClassCode
Name
%ConvertToSubClassCode Start of a to-sub-class code _block_
Synopsis
%ConvertToSubClassCode
602
Appendix C. First Steps with Sip
class X
{
...
PyMethods:
__str__
%MemberCode
C++ code (no enclosing braces needed).
Number and kind of arguments and re-
turn value depend on method
(see specialPyMethod in sip.h).
%End
...
}
PyMethods
Name
PyMethods Implement additional Python special methods
Synopsis
PyMethods
603
Appendix C. First Steps with Sip
Description
Within this section the following methods are legal:
__repr__, __str__, __cmp__, __hash__, __call__, __richcompare__
PyNumberMethods
Name
PyNumberMethods Implement numerical Python special methods
Synopsis
PyNumberMethods
Description
Within this section the following methods are legal:
__add__, __sub__, __mul__, __div__, __mod__, __divmod__, __pow__, __neg__,
__pos__, __abs__, __nonzero__, __invert__, __lshift__, __rshift__, __and__,
__xor__, __or__, __coerce__, __int__, __long__, __float__, __oct__, __hex__,
__iadd__, __isub__, __imul__, __idiv__, __imod__, __ipow__, __ilshift__,
__irshift__, __iand__, __ixor__, __ior__
604
Appendix C. First Steps with Sip
PySequenceMethods
Name
PySequenceMethods Implement Python sequence special methods
Synopsis
PySequenceMethods
Description
Within this section the following methods are legal:
__len__, __add__, __mul__, __getitem__, __setitem__, __getslice__, __setslice__,
__contains__, __iadd__, __imul__
PyMappingMethods
Name
PyMappingMethods Implement Python mapping special methods
Synopsis
PyMappingMethods
605
Appendix C. First Steps with Sip
Description
Within this section the following methods are legal:
__len__, __getitem__, __setitem__
C.7.8. Other
%Makefile
Name
%Makefile The start of a Makefile code _block_
Synopsis
Description
Multiple %Makefile blocks may be given, each defining a makefile template. Using
sip option -m makefile_name selects and instantiates the appropriate makefile
definition (the one with the same makefile_name). At this time contained
macros are replaced by the actual values.
606
Appendix C. First Steps with Sip
class
Member functions
Static functions must be public and cannot be virtual.
The syntax of class method declarations is as follows:
607
Appendix C. First Steps with Sip
protected
If the function is protected, call the public wrapper. Otherwise, explicitly call
the real function and not any version in the wrapper class (in case it is virtual).
This will prevent virtual loops. You dont need to worry about indirected
objects for protected functions.
signals
Arguments must be simple. Otherwise you have to supply your own C++
code.
Virtual signals are not supported.
slots
Arguments must be simple. Otherwise you have to supply your own C++
code.
virtual
608
Appendix C. First Steps with Sip
Arguments must be simple. Otherwise you have to supply your own C++
code.
const
static
609
Appendix C. First Steps with Sip
sipGetCppPtr
Name
sipGetCppPtr Get the C/C++ pointer from a wrapper and cast it to the
required type
Synopsis
Description
Return Value
A pointer to the appropriate C++ class or NULL in case of an error.
w
A pointer to SIPs info about the Python object.
toClass
A pointer to the Python object.
610
Appendix C. First Steps with Sip
sipGetComplexCppPtr
Name
sipGetComplexCppPtr Get the C/C++ pointer for a complex object
Synopsis
Description
No access to protected functions or signals for object not created from Python.
Return Value
A pointer to the appropriate C++ class or NULL in case of an error.
w
A pointer to SIPs info about the Python object.
611
Appendix C. First Steps with Sip
sipGetThisWrapper
Name
sipGetThisWrapper Convert a C/C++ pointer to the object that wraps it
Synopsis
Description
Return Value
Return the wrapped Python object or NULL if it wasnt found.
cppPtr
The C++ pointer, used as a key to SIPs object map.
pyClass
if the wrapped object is a sub-class of the given pyClass then we assume we are
returning the value of something like QObject::parent() where the parent is
something like a QLabel.
612
Appendix C. First Steps with Sip
sipIsSubClassInstance
Name
sipIsSubClassInstance See if a Python object is an instance of a
sub-class of a given base class
Synopsis
Description
Return Value
True if Python object inst is an instance of a sub-class of baseclass, else false.
inst
Pointer to the Python object instance.
baseclass
Pointer to the Python base class.
613
Appendix C. First Steps with Sip
sipParseArgs
Name
sipParseArgs Parse the arguments to a C/C++ function without any side
effects
Synopsis
Description
Return Value
false (or 0) if an error occurred, else true (or 1).
argsParsedp
Parsing stops if an error is encountered or all arguments / format specifiers are
exhausted. The number of arguments parsed so far is stored here along with error
flags.
614
Appendix C. First Steps with Sip
sipArgs
A pointer to a tuple which supplies the arguments to be parsed.
fmt
Format string describing arguments. A leading - in the format string disposes of
the arguments on a successful parse. A | in the format string signifies that the
following arguments are optional. The following format specifiers are recognized:
615
Appendix C. First Steps with Sip
...
A variable number of pointers to the arguments which will take the parsed values.
Examples
if (sip-
ParseArgs(&sipArgsParsed, sipArgs, "s|i", &str, &index))
{
int pos, len;
QRegExp *ptr;
616
Appendix C. First Steps with Sip
return NULL;
pos = ptr -> QRegExp::match(str, index, &len);
return Py_BuildValue("(ii)", pos, len);
}
%End
sipConvertToCpp
Name
sipConvertToCpp Convert a Python instance of a class to a C/C++ object
pointer
Synopsis
Description
Return Value
A pointer to the C++ class or NULL in case of an error (e.g. the instances class is
not derived from the given baseclass).
617
Appendix C. First Steps with Sip
sipSelf
A pointer to the Python object instance.
baseclass
The base class of the Python object instance.
iserrp
Store TRUE here if we had an error.
sipMapCppToSelf
Name
sipMapCppToSelf Convert a C/C++ pointer to a Python instance
Synopsis
618
Appendix C. First Steps with Sip
Description
If the C/C++ pointer is recognized, and it is an instance of a sub-class of the
expected class, then the previously wrapped instance is returned. Otherwise a new
Python instance is created with the expected class. The instance comes with a
reference.
Return Value
A pointer to the Python object instance (or Py_None if a NULL pointer was passed
in).
cppPtr
A pointer to the C++ object.
pyClass
The expexted Python class.
sipConvertToVoidPtr
Name
sipConvertToVoidPtr A convenience function to convert a C/C++ void
pointer from a Python object
619
Appendix C. First Steps with Sip
Synopsis
Description
Return Value
The C/C++ pointer (or NULL if the object was Py_None).
obj
The Python object
sipConvertFromVoidPtr
Name
sipConvertFromVoidPtr A convenience function to convert a C/C++ void
pointer to a Python object
Synopsis
620
Appendix C. First Steps with Sip
Description
Return Value
A pointer to the Python object (or Py_None if val was a NULL pointer).
val
The C/C++ pointer.
sipConvertFromBool
Name
sipConvertFromBool A convenience function to convert a C/C++ boolean
to a Python object
Synopsis
621
Appendix C. First Steps with Sip
Description
Return Value
Py_True or Py_False, depending on val.
val
The value to evaluate.
sipCheckNone
Name
sipCheckNone Check a None argument for a class pointer that we might
dereference
Synopsis
Description
Report a Python runtime error with this message: Cannot pass None as a
classname argument in this call
622
Appendix C. First Steps with Sip
willDeref
If this is TRUE, the error is generated.
isErr
Store TRUE here if the error is generated.
classname
This goes into the error message.
sipBadVirtualResultType
Name
sipBadVirtualResultType Report a Python member function with an
unexpected return type
Synopsis
623
Appendix C. First Steps with Sip
Description
Report a Python type error with this mesage: Invalid result type from
classname.method();
classname
Classname used in error message.
method
Method name used in error message.
sipBadSetType
Name
sipBadSetType Report a Python class variable with an unexpected type
Synopsis
Description
Report a Python type error with this mesage: Invalid type for variable
classname.var
624
Appendix C. First Steps with Sip
classname
Classname used in error message.
var
Variable name used in error message.
sipReleaseLock
Name
sipReleaseLock Release the interpreter lock and save the current Python
thread state
Synopsis
void sipReleaseLock(void);
Description
Calls PyEval_SaveThread().
625
Appendix C. First Steps with Sip
sipAcquireLock
Name
sipAcquireLock Acquire the interpreter lock and restore the Python thread
state
Synopsis
void sipAcquireLock(void);
Description
Calls PyEval_RestoreThread().
sipCondReleaseLock
Name
sipCondReleaseLock Release the interpreter lock, if previously acquired,
and save Python thread state
626
Appendix C. First Steps with Sip
Synopsis
Description
Calls PyEval_SaveThread().
relLock
Release the interpreter lock, if relLock is true, calling PyEval_SaveThread().
sipCondAcquireLock
Name
sipCondAcquireLock Acquire the interpreter lock, if not already acquired,
and restore Python thread state
Synopsis
int sipCondAcquireLock(void);
627
Appendix C. First Steps with Sip
Description
Calls PyEval_RestoreThread().
Return Value
Return TRUE if the lock could be aquired, else FALSE.
sipMalloc
Name
sipMalloc A Python 1.5 style memory allocator that supports Python 1.5 and
1.6
Synopsis
Description
Return Value
Pointer to allocated memory block (or NULL).
628
Appendix C. First Steps with Sip
nbytes
Number of bytes to allocate.
sipFree
Name
sipFree A Python 1.5 style memory de-allocator that supports Python 1.5 and
1.6
Synopsis
Description
mem
A pointer to the memory block to be freed.
629
Appendix C. First Steps with Sip
sipEvalMethod
Name
sipEvalMethod Call a Python method
Synopsis
Description
Return Value
A pointer to the Python object which the methods returns or NULL, if the method
could not be found.
pm
A pointer to SIPs info about the method, usually taken from SIPs method cache.
args
Pointer to a tuple with the parameters.
630
Appendix C. First Steps with Sip
sipCallHook
Name
sipCallHook Call a hook
Synopsis
Description
From the __builtin__ module dictionary get the function hook. Call the hook and
discard any result.
hookname
Character string with the hook function name.
631
Appendix C. First Steps with Sip
sipEmitSignal
Name
sipEmitSignal Emit a Python or Qt signal
Synopsis
Description
Return Value
Return 0 if the signal was emitted or if there was an error.
sig
sigargs
632
Appendix C. First Steps with Sip
sipConvertRx
Name
sipConvertRx Convert a Python receiver to a Qt receiver
Synopsis
Description
Convert a Python receiver (either a Python signal or slot or a Qt signal or slot) to a
Qt receiver. It is only ever called when the signal is a Qt signal.
Return Value
Return NULL is there was an error.
proxyfunc
txThis
sigargs
633
Appendix C. First Steps with Sip
rxobj<
slot
memberp
iserr
Set *iserr to TRUE if there was an error.
sipConnectRx
Name
sipConnectRx Connect a Qt or a Python signal
Synopsis
634
Appendix C. First Steps with Sip
Description
Connect a Qt signal or a Python signal to a Qt slot, a Qt signal, a Python slot or a
Python signal. These are all possible combinations.
Return Value
Py_True or Py_False or NULL, if there was an error.
txobj
sig
rxobj
slot
sipGetRx
Name
sipGetRx Convert a valid Python signal or slot to an existing proxy Qt slot
635
Appendix C. First Steps with Sip
Synopsis
Description
Return Value
NULL if there was an error.
txThis
sigargs
rxobj
slot
memberp
636
Appendix C. First Steps with Sip
iserr
sipDisconnectRx
Name
sipDisconnectRx Disconnect a Qt or Python signal from a Python slot
Synopsis
Description
Return Value
txobj
sig
637
Appendix C. First Steps with Sip
rxobj
slot
638
Bibliography
This bibliography covers the sources I have used while writing this book. Most of
the books are fairly to extremely well known, such as Code Complete or Design
Patters. Its the closest the programming field has to a canon.
A lot of the generally recommended books, such as Fowlers Refactoring are not
really suited to Python. These books (and this holds for Design Patterns as well) are
more concerned with C++ or Java that is, rather low-level languages where the
developer has to do a lot himself.
Gaius Iulius Caesar, 1954, De Bello Gallico, Bewerkt door Dr. J.J.E. Hondius,
Zesde Uitgave.
Craig A. Finseth, 1999, The Craft of Text Editing: or Emacs for the Modern World,
http://www.finseth.com/~fin/craft.
Erich Gamma, Richard Helm, Ralph Johnson, and Jonh Vlissides, 1995, Design
patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley
Publishing Company.
Andrew Hunt, David Thomas, and Ward Cunningham, 2000, The Pragmatic
Programmer: From Journeyman to Master, Addison-Wesley Publishing
Company.
639
Mark Lutz, 1996, Programming Python, OReilly & Associates.
Gareth McCaughan, Rhodri James, and Paul Wright, 2001, Livewires Python
Course, Scripture Union, http://www.livewires.org.uk/python/.
Boudewijn Rempt, Pythons PyQt Toolkit, January 2001, 2001, 88-95, Dr Dobbs
Journal, 320, CMP, Edited by Jonathan Erickson.
van Rossum Guido and Drake Fred L., 2001, Python Language Reference,
http://www.python.org/doc/current/ref/ref.html.
van Rossum Guido and Drake Fred L., 2001, Python Tutorial,
http://www.python.org/doc/current/tut/tut.html.
Aaron Watters, Guido van Rossum, and James C. Ahlstrom, Internet Programming
with Python, 1996, M&T Books.
Pelham Grenville Wodehouse, Much Obliged, Jeeves, 1981 (1971), Penguin Books.
640