GUI Programming with Python: QT Edition

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

GUI Programming with Python: QT Edition
GUI Programming with Python: QT Edition

Boudewijn Rempt

Copyright © 2001 by Commandprompt, Inc

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.

Table of Contents
Preface
Who is using PyQt
For whom is this book intended
How to read this book
Conventions
Acknowledgments
1. Introduction
Python
GUI programming with Python
About the BlackAdder IDE
I. Introduction to the BlackAdder IDE
2. Installation
Installing BlackAdder
Installing sip and PyQt without BlackAdder
3. Interface
Menubar
Toolbars
Project management
BlackAdder Configuration
Editing
Python shell
Conclusion
4. Introduction to Python
Programming fundamentals
The Rules
Constructions
Conclusion
5. Debugging
Running scripts
Setting breakpoints
Stepping along
Debugging Techniques
If all else fails
II. PyQt fundamentals
6. Qt Concepts
Python, Qt and PyQt
As simple as they come
A better Hello World
Designing forms
Conclusion
7. Signals and Slots in Depth
The concept of signals and slots
Connecting with signals and slots
Disconnecting
A parser-formatter using signals and slots
Conclusion
8. String Objects in Python and Qt
Introduction
String conversions
QCString — simple strings in PyQt
Unicode strings
9. Python Objects and Qt Objects
Pointers and references
Circular references
Qt objects, Python objects and shadow objects
References and ownership
Other C++ objects
Connecting signals and slots
Object and class introspection
10. Qt Class Hierarchy
Hierarchy
Base classes
Application classes
Widget foundations: QWidget
Basic widgets
Advanced widgets
Layout managers
Dialogs and Standard Dialogs
Qt Utility classes and their Python equivalents
11. Qt Designer, BlackAdder and uic
Introduction
Advanced Designer topics
III. Creating real applications with PyQt
12. Application Frameworks
Architecture: models, documents and views
Macro languages
Project layout
13. Actions: menus, toolbars and accelerators
Actions
Menus
Toolbars
Keyboard accelerators
Setting an application icon
14. Automatic testing with PyUnit
About unittests
Starting out
A first testcase
Collecting tests in a test suite
A more complicated test
Large projects
Testing signals and slots
Conclusion
15. A More Complex Framework: Multiple Documents, Multiple Views
Introduction
Document/View Manager
The Document Manager
Document
View
The actual application
Conclusion
16. User Interface Paradigms
Tabbed documents
Back to the MDI windows
A row of split windows
A stack of documents
A more complex view management solution
Conclusion
17. Creating Application Functionality
Introduction
The view
The document
Saving and loading documents
Undo, redo and other editing functions
Conclusion
18. Application Configuration
Platform differences
The Python way of handling configuration settings
Implementing configurations settings for Kalam
Settings in Qt 3.0
Conclusion
19. Using Dialog Windows
Modal: a preferences dialog
Non-modal: Search and replace
Conclusion
20. A Macro Language for Kalam
Executing Python code from Python
Integrating macros with a GUI
Creating a macro API from an application
Conclusion
21. Drawing on Painters and Canvases
Working with painters and paint devices
QCanvas
Conclusion
22. Gui Design in the Baroque Age
Types of gui customization
Faking it with bitmaps
Creating themes with QStyle
23. Drag and drop
Handling drops
Initiating drags
Conclusion
24. Printing
The QPrinter class
Adding printing to Kalam
Putting ink to paper
Conclusion
25. Internationalizing an Application
Translating screen texts
26. Delivering your Application
Introduction
Packaging source
Starting with distutils.
Creating Unix RPM packages
Windows installers
Desktop integration
27. Envoi
IV. Appendices
A. Reading the Qt Documentation
B. PyQwt: Python Bindings for Qwt
NumPy
PyQwt
C. First Steps with Sip
Introduction
How sip works
Creating .sip files
Things sip can't do automatically
Where to look to start writing your own wrappers/bindings
Sip usage and syntax
Directives
Accepted C++ / Qt constructs
SIPLIB Functions
Bibliography
Index
List of Tables
1-1. GUI Toolkits for Python
7-1. Matrix of QObject.connect() combinations.
10-1. Qt and Python high-level datastructures
10-2. Qt and Python network classes
C-1. C++ access specifiers and sip
C-2. Command line options
C-3. Macros in Makefile Templates
C-4. Flags in member function declaration
C-1. Error flags in sipParseArgs()
C-2. Format specifiers for sipParseArgs()
List of Figures
10-1. Qt Inheritance Hierarchy (only the most important classes)
10-2. Object Ownership Hierarchy
20-1. Playing with eval()
20-2. Playing with exec
20-3. Playing with execfile()
List of Examples
1-1. Bootstrapping a Python application
6-1. hello1.py — hello world
6-2. hello2.py — a better hello world
6-3. fragment from hello3.py
6-4. Fragment from hello5.py
6-5. Fragment from hello4.py
6-6. frmconnect.py
6-7. dlgconnect.py — the subclass of the generated form
7-1. A stupid button which is not reusable
7-2. A simple callback system
7-3. A central registry of connected widgets
7-4. Connecting a signal to a slot
7-5. Connection a dial to a label with signals and slots
7-6. Python signals and slots
7-7. Python signals and slots with arguments
7-8. datasource.py — connecting and disconnecting signals and slots
7-9. An XML parser with signals and slots
8-1. qstring1.py — conversion from QString to a Python string.
8-2. qstring2.py - second try of saving a QString to a file
8-3. empty.py - feeding zero bytes to a QCString
8-4. null.py - empty and null QCStrings and Python strings
8-5. emptyqstring.py - feeding zero bytes to a QString
8-6. Loading an utf-8 encoded text
8-7. Building a string from single Unicode characters
8-8. sitecustomize.py — saving a useful function from wanton destruction
8-9. uniqstring3.py - messing with Unicode strings using utf-8 as default encoding
8-10. uniqstring1.py - coercing Python strings into and from QStrings
8-11. uniqstring2.py - coercing Python strings into and from QStrings
9-1. refs.py - showing object references
9-2. circular.py - circululululular references
9-3. qtrefs1.py — about Qt reference counting
9-4. qtrefs2.py - keeping a Qt widget alive
9-5. qtrefs3.py - Qt parents and children
9-6. Eradicating a widget
9-7. children.py - getting the children from a single parent
9-8. Iterating over children
9-9. sigslot.py - a simple signals/slots implementation in Python, following the Observer pattern
9-10. Object introspection using Qt
9-11. Object introspection using Python
10-1. event1.py - handling mouse events in PyQt
10-2. action.py - Using a QAction to group data associated with user commands
10-3. fragment from mdi.py - ten little scribbling windows
10-4. event2.py - using QWidget to create a custom, double-buffered drawing widget.
10-5. snippet from event3.py - a peach puff drawing board
10-6. fragment from action2.py - You cannot create a QPixmap before a QApplication
10-7. buttons.py - Four pushbuttons saying ‘hello'.
10-8. label.py - a label associated with an edit control
10-9. radio.py - a group of mutually exclusive options
10-10. listbox.py - A listbox where data can be associated with an entry
10-11. tree.py - building a tree
10-12. layout.py - two box layouts and adding and removing buttons dynamically to a layout
10-13. geometry.py - setting the initial size of an application
10-14. dialogs.py - opening message and default dialogs boxes
10-15. fragment from dialogs.py - opening a file dialog
10-16. fragment from dialogs.py - opening a font dialog
10-17. fragment from dialogs.py - opening a color dialog
10-18. from dv_qt.py - using Qt utility classes
10-19. fragment from db_python.py - using Python utility classes
10-20. Using QMimeSourceFactory (application.py)
10-21. thread1.py — Python threads without gui
10-22. Python threads and a PyQt gui window
11-1. dlgcomplex.py — a subclass of frmcomplex.py
11-2. Setting default values
12-1. A simple document-view framework
12-2. Scripting an application is easy
13-1. Defining a complex toggle action
15-1. A testcase for a document manager
15-2. The document manager class
15-3. The document class
15-4. The view class
15-5. The application class
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...
21-2. charmap.py - a Unicode character selection widget
22-1. remote.py - remote control application
22-2. view.py - the main view of the remote control application
22-3. button.py - the class that implements the pixmapped buttons
22-4. A Qt 2 custom style - a minimalist implementation of the classic Mac style in PyQt.
22-5. Testing styles
23-1. Handling drop events
23-2. Drag and drop
25-1. Installing the translator
26-1. README
26-2. setup.py - a sample setup script
26-3. MANIFEST.in
C-1. Interface for QRegExp::match
Next
Preface


PyQwt: Python Bindings for Qwt

Prev Next

Appendix B. PyQwt: Python Bindings for Qwt

Table of Contents
NumPy
PyQwt
Gerard Vermeulen

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 you'd 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.

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 Excel—it 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.

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

root@calcifer:/home/boud# python setup_all.py install

Once numpy is installed, you can start Python (or open the Interpreter window in BlackAdder) and import the NumPy extension:

[packer@slow packer]$ python
Python 2.1.1 (#1, Aug 20 2001, 08:17:33)
[GCC 2.95.3 19991030 (prerelease)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> from Numeric import *
>>>

A NumPy array looks like a list and can be created from a list (in fact, from any sequency type: list, tuple or string).

Let's create and print a 1-dimensional NumPy array of Python floats:

>>> a = array([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:

>>> b = array([[0.0, 1.0], [2.0, 3.0]])
>>> print b
[[ 0.  1.]
 [ 2.  3.]]
>>>

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 = array([[[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:

>>> ax = arange(0.0, 5.0, 0.5)
>>> print ax
[ 0.   0.5  1.   1.5  2.   2.5  3.   3.5  4.   4.5]
>>>

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:

def lrange(start, stop, step):
    start, stop, step = float(start), float(stop), float(step)
    size = int(round((stop-start)/step))
    result = [start] * size
    for i in xrange(size):
        result[i] += i * step
    return result

After copying and pasting the function definition in your Python interpreter, do:

>>> lx = lrange(0.0, 5.0, 0.5)
>>> print lx
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5]

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:

>>> ly = map(lorentzian, lx)

NumPy arrays are even more elegant, and they allow:

>>> ay = lorentzian(ax)

Almost magic, isn't 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 ?
  File "<stdin>", line 2, in lorentzian
TypeError: unsupported operand type(s) for -
>>>

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):

>>> import profile
>>> xl = lrange(0, 10, 0.0001)
>>> xa = arange(0, 10, 0.0001)
>>> profile.run('yl = map(lorentzian, xl)')
         100002 function calls in 2.200 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   100000    1.000    0.000    1.000    0.000 <stdin>:1(lorentzian)
        1    1.200    1.200    2.200    2.200 <string>:1(?)
        0    0.000             0.000          profile:0(profiler)
        1    0.000    0.000    2.200    2.200 profile:0(yl = map(lorentzian, xl))

>>> profile.run('ya = lorentzian(xa)')
         3 function calls in 0.090 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.090    0.090    0.090    0.090 <stdin>:1(lorentzian)
        1    0.000    0.000    0.090    0.090 <string>:1(?)
        0    0.000             0.000          profile:0(profiler)
        1    0.000    0.000    0.090    0.090 profile:0(ya = lorentzian(xa))

>>>

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.

Prev | Home | Next
Reading the Qt Documentation | Up | PyQwt


First Steps with Sip

Prev Next

Appendix C. First Steps with Sip

Table of Contents
Introduction
How sip works
Creating .sip files
Things sip can't do automatically
Where to look to start writing your own wrappers/bindings
Sip usage and syntax
Directives
Accepted C++ / Qt constructs
SIPLIB Functions
Jim Bublitz

Jim Bublitz knows far more about sip, the tool used to wrap C++ libraries for Python, than I do. For instance, he's the author of the bindings to the KDE2 libraries. For these reasons, I asked him to write an appendix on using sip.

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 isn't 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 isn't a translation of Qt into Python—instead, 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 it's 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:

  • Re-implementation of C++ virtual methods in Python.

  • Sub-classing C++ classes from Python.

  • Access to a C++ class's protected methods.

  • Overloading of C++ functions and methods with different parameter type signatures.

  • Automatic translation between C++ classes and similar (but more appropriate) Python types.

Prev | Home | Next
PyQwt | Up | How sip works


Boudewijn Rempt

Copyright © 2001 by Commandprompt, Inc

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.

Table of Contents
Preface
Who is using PyQt
For whom is this book intended
How to read this book
Conventions
Acknowledgments
1. Introduction
Python
GUI programming with Python
About the BlackAdder IDE
I. Introduction to the BlackAdder IDE
2. Installation
Installing BlackAdder
Installing sip and PyQt without BlackAdder
3. Interface
Menubar
Toolbars
Project management
BlackAdder Configuration
Editing
Python shell
Conclusion
4. Introduction to Python
Programming fundamentals
The Rules
Constructions
Conclusion
5. Debugging
Running scripts
Setting breakpoints
Stepping along
Debugging Techniques
If all else fails
II. PyQt fundamentals
6. Qt Concepts
Python, Qt and PyQt
As simple as they come
A better Hello World
Designing forms
Conclusion
7. Signals and Slots in Depth
The concept of signals and slots
Connecting with signals and slots
Disconnecting
A parser-formatter using signals and slots
Conclusion
8. String Objects in Python and Qt
Introduction
String conversions
QCString — simple strings in PyQt
Unicode strings
9. Python Objects and Qt Objects
Pointers and references
Circular references
Qt objects, Python objects and shadow objects
References and ownership
Other C++ objects
Connecting signals and slots
Object and class introspection
10. Qt Class Hierarchy
Hierarchy
Base classes
Application classes
Widget foundations: QWidget
Basic widgets
Advanced widgets
Layout managers
Dialogs and Standard Dialogs
Qt Utility classes and their Python equivalents
11. Qt Designer, BlackAdder and uic
Introduction
Advanced Designer topics
III. Creating real applications with PyQt
12. Application Frameworks
Architecture: models, documents and views
Macro languages
Project layout
13. Actions: menus, toolbars and accelerators
Actions
Menus
Toolbars
Keyboard accelerators
Setting an application icon
14. Automatic testing with PyUnit
About unittests
Starting out
A first testcase
Collecting tests in a test suite
A more complicated test
Large projects
Testing signals and slots
Conclusion
15. A More Complex Framework: Multiple Documents, Multiple Views
Introduction
Document/View Manager
The Document Manager
Document
View
The actual application
Conclusion
16. User Interface Paradigms
Tabbed documents
Back to the MDI windows
A row of split windows
A stack of documents
A more complex view management solution
Conclusion
17. Creating Application Functionality
Introduction
The view
The document
Saving and loading documents
Undo, redo and other editing functions
Conclusion
18. Application Configuration
Platform differences
The Python way of handling configuration settings
Implementing configurations settings for Kalam
Settings in Qt 3.0
Conclusion
19. Using Dialog Windows
Modal: a preferences dialog
Non-modal: Search and replace
Conclusion
20. A Macro Language for Kalam
Executing Python code from Python
Integrating macros with a GUI
Creating a macro API from an application
Conclusion
21. Drawing on Painters and Canvases
Working with painters and paint devices
QCanvas
Conclusion
22. Gui Design in the Baroque Age
Types of gui customization
Faking it with bitmaps
Creating themes with QStyle
23. Drag and drop
Handling drops
Initiating drags
Conclusion
24. Printing
The QPrinter class
Adding printing to Kalam
Putting ink to paper
Conclusion
25. Internationalizing an Application
Translating screen texts
26. Delivering your Application
Introduction
Packaging source
Starting with distutils.
Creating Unix RPM packages
Windows installers
Desktop integration
27. Envoi
IV. Appendices
A. Reading the Qt Documentation
B. PyQwt: Python Bindings for Qwt
NumPy
PyQwt
C. First Steps with Sip
Introduction
How sip works
Creating .sip files
Things sip can't do automatically
Where to look to start writing your own wrappers/bindings
Sip usage and syntax
Directives
Accepted C++ / Qt constructs
SIPLIB Functions
Bibliography
Index
List of Tables
1-1. GUI Toolkits for Python
7-1. Matrix of QObject.connect() combinations.
10-1. Qt and Python high-level datastructures
10-2. Qt and Python network classes
C-1. C++ access specifiers and sip
C-2. Command line options
C-3. Macros in Makefile Templates
C-4. Flags in member function declaration
C-1. Error flags in sipParseArgs()
C-2. Format specifiers for sipParseArgs()
List of Figures
10-1. Qt Inheritance Hierarchy (only the most important classes)
10-2. Object Ownership Hierarchy
20-1. Playing with eval()
20-2. Playing with exec
20-3. Playing with execfile()
List of Examples
1-1. Bootstrapping a Python application
6-1. hello1.py — hello world
6-2. hello2.py — a better hello world
6-3. fragment from hello3.py
6-4. Fragment from hello5.py
6-5. Fragment from hello4.py
6-6. frmconnect.py
6-7. dlgconnect.py — the subclass of the generated form
7-1. A stupid button which is not reusable
7-2. A simple callback system
7-3. A central registry of connected widgets
7-4. Connecting a signal to a slot
7-5. Connection a dial to a label with signals and slots
7-6. Python signals and slots
7-7. Python signals and slots with arguments
7-8. datasource.py — connecting and disconnecting signals and slots
7-9. An XML parser with signals and slots
8-1. qstring1.py — conversion from QString to a Python string.
8-2. qstring2.py - second try of saving a QString to a file
8-3. empty.py - feeding zero bytes to a QCString
8-4. null.py - empty and null QCStrings and Python strings
8-5. emptyqstring.py - feeding zero bytes to a QString
8-6. Loading an utf-8 encoded text
8-7. Building a string from single Unicode characters
8-8. sitecustomize.py — saving a useful function from wanton destruction
8-9. uniqstring3.py - messing with Unicode strings using utf-8 as default encoding
8-10. uniqstring1.py - coercing Python strings into and from QStrings
8-11. uniqstring2.py - coercing Python strings into and from QStrings
9-1. refs.py - showing object references
9-2. circular.py - circululululular references
9-3. qtrefs1.py — about Qt reference counting
9-4. qtrefs2.py - keeping a Qt widget alive
9-5. qtrefs3.py - Qt parents and children
9-6. Eradicating a widget
9-7. children.py - getting the children from a single parent
9-8. Iterating over children
9-9. sigslot.py - a simple signals/slots implementation in Python, following the Observer pattern
9-10. Object introspection using Qt
9-11. Object introspection using Python
10-1. event1.py - handling mouse events in PyQt
10-2. action.py - Using a QAction to group data associated with user commands
10-3. fragment from mdi.py - ten little scribbling windows
10-4. event2.py - using QWidget to create a custom, double-buffered drawing widget.
10-5. snippet from event3.py - a peach puff drawing board
10-6. fragment from action2.py - You cannot create a QPixmap before a QApplication
10-7. buttons.py - Four pushbuttons saying ‘hello'.
10-8. label.py - a label associated with an edit control
10-9. radio.py - a group of mutually exclusive options
10-10. listbox.py - A listbox where data can be associated with an entry
10-11. tree.py - building a tree
10-12. layout.py - two box layouts and adding and removing buttons dynamically to a layout
10-13. geometry.py - setting the initial size of an application
10-14. dialogs.py - opening message and default dialogs boxes
10-15. fragment from dialogs.py - opening a file dialog
10-16. fragment from dialogs.py - opening a font dialog
10-17. fragment from dialogs.py - opening a color dialog
10-18. from dv_qt.py - using Qt utility classes
10-19. fragment from db_python.py - using Python utility classes
10-20. Using QMimeSourceFactory (application.py)
10-21. thread1.py — Python threads without gui
10-22. Python threads and a PyQt gui window
11-1. dlgcomplex.py — a subclass of frmcomplex.py
11-2. Setting default values
12-1. A simple document-view framework
12-2. Scripting an application is easy
13-1. Defining a complex toggle action
15-1. A testcase for a document manager
15-2. The document manager class
15-3. The document class
15-4. The view class
15-5. The application class
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...
21-2. charmap.py - a Unicode character selection widget
22-1. remote.py - remote control application
22-2. view.py - the main view of the remote control application
22-3. button.py - the class that implements the pixmapped buttons
22-4. A Qt 2 custom style - a minimalist implementation of the classic Mac style in PyQt.
22-5. Testing styles
23-1. Handling drop events
23-2. Drag and drop
25-1. Installing the translator
26-1. README
26-2. setup.py - a sample setup script
26-3. MANIFEST.in
C-1. Interface for QRegExp::match
Next
Preface


Introduction

Prev Next

Chapter 1. Introduction

Table of Contents
Python
GUI programming with Python
About the BlackAdder IDE

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. More's the pity that there aren't 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.

It's amazing how many software development environments have been designed with something else than developer productivity as the main goal. There's 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 doesn't 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.

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 it's ‘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 don't need pre-processors or compilers, you can get by with nothing more than a text editor, like XEmacs Nedit, or MultiEdit. Indeed, I've 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 Apple's OS X, too).

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:

  • Compiled to byte-code, interpreted by a virtual machine.

  • High-level data structures: lists, tuples and dictionaries

  • Dynamic: you can even add new base-classes to an existing object, run-time.

  • Portable: the same Python bytecode will run — depending on which version of Python you use and which C or C++ extensions are used — on Unix, Windows, MacOS, Amiga, Palm OS and many others.

  • Extensible with modules written in C or C++: there is no performance penalty for calling native code, as there is when calling native code from Java.

  • An object-oriented programming model, but also supports functional programming (a bit) and old-fashioned structured programming.

  • Enormous set of extension libraries: for database access, high-performance number-crunching, for sound-file analysis, for GUI programming and countless other tasks.

  • Built-in regular expression engine that works on both regular and Unicode strings.

  • Use of indentation instead of braces begin/end pairs to delimit blocks of code. This practically forces readable code.

Your Python code resides in files, ending with .py suffix. These files can be grouped in modules, in the form of directories with an indexfile called __init__.py, and you can import elements from modules and files in other files. There is one file you use to start your application. It will usually simply import the necessary modules and start the application explicitly in a main (args) function.

Maybe the introduction is bit early to start with actual code examples, but let's have an example of a Python bootstrap script anyway:

Example 1-1. Bootstrapping a Python application

#!/usr/bin/env python                                        (1)
#
# bootstrap.py
#

import sys                                                   (2)
from myapp import SomeClass                                  (3)

def main(args):                                              (4)
    class=SomeClass(args)
    class.exec_loop()

if __name__=="__main__":                                     (5)
    main(sys.argv)
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 won't 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 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.

Prev | Home | Next
Acknowledgments |  | GUI programming with Python


Installation

Prev Next

Chapter 2. Installation

Table of Contents
Installing BlackAdder
Installing sip and PyQt without BlackAdder

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 buying—so don't forget to read the README files!

Installing BlackAdder

PyQt can be used on Windows, Unix/X11 and soon OS X, but BlackAdder is only available for Windows and Unix/X11 (due to licensing restrictions).

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 it's simply a matter of installing the components, one after another. Every component is provided in a comfortable Windows installer package.

Installing BlackAdder

BlackAdder will now be ready to run — a friendly icon has appeared on your desktop, just begging to be clicked.

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

boud@calcifer:~/tmp > rpm --install BA-personal-1.0Beta3-1_tkc_suse71.i386.rpm

on the command line, if you're installing the 3.1 beta for SuSE Linux. The actual name of the rpm will vary, of course.

KPackage with the BlackAdder rpm

Additionally, you might want to set two environment variables in your .bashrc file. Installing BlackAdder and Python in the default location isn't necessary, but if you deviate from the standard BlackAdder installation directory, you need to set the following variables.

export BLACKADDERDIR=/usr/lib/BlackAdder export
BLACKADDERPYTHON=/usr/local/bin/python

Now, simply typing "ba" on the command line will start BlackAdder.

Prev | Home | Next
Introduction to the BlackAdder IDE | Up | Installing sip and PyQt without BlackAdder


Interface

Prev Next

Chapter 3. Interface

Table of Contents
Menubar
Toolbars
Project management
BlackAdder Configuration
Editing
Python shell
Conclusion

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. You'll 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.

BlackAdder and its ‘About' dialog.

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 interpreter executable (either python or python.exe on Windows), as described in the Section called Linux in Chapter 2.

Python has given the ghost.

However, if all is well, you can start exploring your new development environment.

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.

Prev | Home | Next
Installing sip and PyQt without BlackAdder | Up | Toolbars


Introduction to Python

Prev Next

Chapter 4. Introduction to Python

Table of Contents
Programming fundamentals
The Rules
Constructions
Conclusion

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 doesn't understand plain English, I'm 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 can't really be done, I'm 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 haven't 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.

Programming fundamentals

Please don't think that I can teach you programming in just the space of this section — you need to read some good books for that, such as Steve McConnel's Code Complete. What I can do is show you what the fuss is all about.

Computers do not do anything of their own volition: ultimately, someone has 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 you've 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.

Let's 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 can't 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 don't do this! On the other hand, asking Python about the captain's age or the contents of a bathtub that's 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.

Let's 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 don't type the three > signs or the three dots — Python does this for you.

Python 2.1.1 (#1, Aug 11 2001, 20:14:53) [GCC 2.95.2 19991024
(release)] on linux2 Type "copyright", "credits" or "license"
for more information.
>>> class FirstClass:
...     def __init__(self, value):
...             self.item=value
...     def printValue(self):
...             print self.item
...
>>> firstObject=FirstClass(value="BlackAdder goes forth")
>>> firstObject.printValue
<method FirstClass.printValue of FirstClass instance at 0x80db1f4>
>>> firstObject.printValue()
BlackAdder goes forth
>>>

If you type neatly and without mistakes, the contents of the Python interpreter window might look like this. Let's look at what happens: we have defined a ‘class' — that's 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 — it's 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 doesn't 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 number) ‘BlackAdder goes forth', you can later call the printValue() function, which will be able to do something with that value.

In order to call—that is, ask Python to execute— a function, you must add brackets after the function name; the parameters always go between the brackets. You don't have to put self between brackets, for Python does this for you. If you don't 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 instance 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 BlackAdder's 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 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, don't 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 I've ever read.

Prev | Home | Next
Conclusion | Up | The Rules


Debugging

Prev Next

Chapter 5. Debugging

Table of Contents
Running scripts
Setting breakpoints
Stepping along
Debugging Techniques
If all else fails

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, really—in most cases it's 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 we'll use the debugger included in BlackAdder to find bugs in some simple scripts. If you don't understand the actual code yet, don't 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 you've ever tried to actually trace an application using one of these, you'll 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:

Python 2.1.1 (#1, Aug 11 2001, 20:14:53)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> import pdb
>>> import button
>>> pdb.run("button.main([])")
> /home/boudewijn/doc/pyqt/ch23/<string>(0)?()
(Pdb) continue
> /home/boudewijn/doc/pyqt/ch23/<string>(1)?()
(Pdb) continue
Traceback (most recent call last):
  File "button.py", line 26, in slotSlot
    i = 1/0
ZeroDivisionError: integer division or modulo by zero
--Return--
> /home/boudewijn/doc/pyqt/ch23/<string>(1)?()->None
(Pdb) w
> /home/boudewijn/doc/pyqt/ch23/<string>(1)?()->None
(Pdb) l
[EOF]
(Pdb) q
>>>

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 don't know where to stop debugging. If you are debugging a piece of code that contains the statement string.join(), you probably don't want to single-step into the string.py module, which is part of the Python system library—and 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.

The BlackAdder debugger.

The PyQt library includes another, simpler debugger, called eric. This application is no longer maintained, so I won't spend much time here describing the way it works. It does, however, provide a very nice example of using regular expressions and PyQt's QCanvas widget. You can start eric by typing eric on the command-line.

Eric, the debugger included in PyQt.

Running scripts

The BlackAdder toolbar has two buttons for debugging your code. You can either debug a single script, or the whole project.

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.

Prev | Home | Next
Conclusion | Up | Setting breakpoints


Qt Concepts

Prev Next

Chapter 6. Qt Concepts

Table of Contents
Python, Qt and PyQt
As simple as they come
A better Hello World
Designing forms
Conclusion

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.

Python, Qt and PyQt

Unlike a tool like Visual Basic, which consists of a GUI engine with a scripting language built-in, Python does not have a native GUI interface. But there are many GUI libraries available for Python — examples are wxPython, Tkinter, PyGTK, PyFLTK, FoxPy, and PyQt. PyQt is based on Qt, an advanced GUI library for Windows and Unix written in C++ by Eirik Eng and Arnt Gulbrantsen of Trolltech in Norway. It's quite easy to wrap C++ or C libraries so they can be used from Python — and when Phil Thompson was looking around for a good GUI library for Python he decided to wrap Qt, producing PyQt. PyQt forms the basis for the BlackAdder rapid development environment.

Qt is very advanced: the library offers a large set of well-designed screen objects, or widgets, and many utility classes. In addition, Qt has a clean object-oriented design that is easy to grasp and intuitive to use. PyQt applications can run without any change, without recompiling, both on Windows and Unix/X11 systems — and soon on Apple's OS X, too.

PyQt widgets can be drawn in styles, to make them appear exactly like the native widgets of the operating system the application runs on (or like something different altogether, if you want).

There are two kinds of objects in the Qt library— visual and non-visual. The mother of all visual objects is QWidget, widget being the term Qt uses for what the Windows world usually calls control. There are simple widgets such as labels and buttons, and complex widgets such as canvas views. Even dialog windows are descended from QWidget.

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 you'll have a working application where you would still be hacking the first prototype in C++.

A note on versions: PyQt consists of at least three components, Python, Qt and PyQt itself. Additionally, there is PyKDE, the bindings to the KDE Desktop Environment for Unix. That's four components with almost unrelated version numbering. Qt has been through three versions to date, just like PyQt. However, PyQt 3.x can be compiled against Qt 1.x, Qt 2.x and Qt 3.x.

The differences between versions of PyQt consist of the range of versions of Qt supported, and certain matters of organization and some implementation details.

The relation between PyQt and Python is even more nebulous: PyQt works best with the latest version of Python, but earlier versions are known to work, too.

Anyway, just keep in mind that PyQt 3.x can use Qt 2.x or Qt 3.x. This book was mostly written using PyQt 2.x for Qt 2.x, but I've tried to include as much information about PyQt 3.x and Qt 3.x as I could.

Prev | Home | Next
PyQt fundamentals | Up | As simple as they come


Signals and Slots in Depth

Prev Next

Chapter 7. Signals and Slots in Depth

Table of Contents
The concept of signals and slots
Connecting with signals and slots
Disconnecting
A parser-formatter using signals and slots
Conclusion

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.

The concept of signals and slots

The problem in a nutshell: imagine you have an application, and the application shows a button on screen. Whenever the button is pressed, you want a function in your application to execute. Of course, you'd prefer that the button doesn't know much about the application, or you would have to write a different button for each application. In the next example, the button has been coded to work only with an application that has the doSomeApplicationSpecificFunction function.

Example 7-1. A stupid button which is not reusable

#
# stupid_button.py — this button is not reusable
#
class Button:

    def __init__(self, application):
      self.application = application

    def clicked(self):
        self.application.doSomeApplicationSpecificFunction()

class Application:

    def __init__(self):
        self.button=Button(self)

    def doSomeApplicationSpecificFunction(self):
        print "Function called"

app=Application()
app.button.clicked() # simulate a user button press

Callbacks

This is no solution— the button code isn't 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.

Example 7-2. A simple callback system

#
# callback.py — handing the function over the the app
#
class Button:

    def __init__(self, function):
        self.callbackFunction = function

    def clicked(self):
        apply(self.callbackFunction)

class Application:

    def __init__(self):
        self.button=Button(self.doSomeApplicationSpecificFunction)

    def doSomeApplicationSpecificFunction(self):
        print "Function called"

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(). You'll see that idiom quite often when we subclass Qt classes:

class MyWidget(QWidget):

    def __init__(self, *args):
        apply(QWidget.__init__, (self,) + args)

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):

    def __init__(self, parent):
        QWidget.__init__(self, parent)

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 the Section called Methods and functions in Chapter 4 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 don't 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:

>>> class O(QObject):
...     def __init__(self, *args):
...             QObject.__init__(self, *args)
...
>>> 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.

Action registry

Unfortunately, this callback system is not quite generic enough. For example, what if you wanted to activate two functions when the button is pressed? While this is not likely in the simple example, under more complex situations it often occurs. Think of a text editor where editing the text should change the internal representation of the document, the word count in the statusbar, and the edited-indicator in the titlebar. You wouldn't want to put all this functionality in one function, but it is a natural fit for signals and slots. You could have one signal, textChanged, that is connected to three functions: changeText(), setWordCount(), setEdited().

Wouldn't it be extremely comfortable to simply have a central registry where interested parties could come together? Something like:

Example 7-3. A central registry of connected widgets

#
# registry.py — a central registry of connected widgets
#
class Registry:

    def __init__(self):
        self.connections={}                                  (1)

    def add(self, occasion, function):                       (2)
        if self.connections.has_key(occasion) == 0:
            self.connections[occasion]=[function]
        else:
            self.connections[occasion].append(function)

    def remove(self, occasion, function):                    (3)
        if self.connections.has_key(occasion):
            self.connections[occasion].remove(function)

    def execute(self, occasion):
        if self.connections.has_key(occasion):               (4)
            for function in self.connections[occasion]:
                apply(function)

registry=Registry()
                                                             (5)
class Button:
                                                             (6)
    def clicked(self):
        registry.execute("clicked")

class Application:

    def __init__(self):
        self.button=Button()
        registry.add("clicked", self.doAppSpecificFunction)
        registry.add("clicked", self.doSecondFunction)       (7)

    def doAppSpecificFunction(self):
        print "Function called"

    def doSecondFunction(self):
        print "A second function is called."

app=Application()
app.button.clicked()
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 wouldn't 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.

Signals and slots

We've just outlined the problem which the developers of Qt at Trolltech have solved in a unique and flexible manner. They created the concept of signals and slots. signals are sent by an object that wants to tell the world something interesting has happened, and by connecting the "signals to the slots", those signals arrive at the slots of the objects that are interested.

On the whole the concept is really neat and clean and the implementation well-executed. What's more, the concept is even better suited to Python than it is to C++. If you want to use signals and slots in C++, you have to work with a preprocessor, called moc, and indicate with special macros which function can be called as a slot, and which function is a signal. All that is completely unnecessary in Python, where a signal is a string, and any function can be a slot.

Signals and slots are not magic, of course. As with our simple Python registry, there has to be a registry of objects that are interested in signals. This registry is updated by the connect and disconnect functions; both are member functions of the QObject class. The registry, as far as Python signals and slots is concerned, is kept by the sip library. Signals and slots that are defined in the underlying C++ library are maintained by the QObject class itself.

In a nutshell, signals and slots are the solution Qt provides for situations in which you want two objects to interact, while keeping that fact hidden from them.

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 don't connect two class instances, but rather a physical object, such as a keyboard, with an 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.

Prev | Home | Next
Conclusion | Up | Connecting with signals and slots


String Objects in Python and Qt

Prev Next

Chapter 8. String Objects in Python and Qt

Table of Contents
Introduction
String conversions
QCString — simple strings in PyQt
Unicode strings

Most likely, you won't need the information in this chapter very often. If you don't 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 PyQt's behavior might surprise you. Then you will probably find yourself coming to this chapter.

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 doesn't 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 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.

Prev | Home | Next
Conclusion | Up | String conversions


Python Objects and Qt Objects

Prev Next

Chapter 9. Python Objects and Qt Objects

Table of Contents
Pointers and references
Circular references
Qt objects, Python objects and shadow objects
References and ownership
Other C++ objects
Connecting signals and slots
Object and class introspection

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 you're leaking memory like a sieve, then this is the place to turn to.

Pointers and references

In order to be able to determine the relations between Python objects and C++ objects it is necessary to first gain a good understanding of what an object is, exactly, and what constitutes a reference to an object.

In C++, an object is simply a chunk of memory that contains executable bytes and data bytes. The executable bytes represent the functions, and the data bytes represent the values of the object variables. Of course, this is a simplified representation: the functions are shared by all objects of the same class, and there is some serious (and platform dependent) pointer logic needed to find them. But, basically, a C++ object is simply a stretch of memory that has to be allocated explicitly by the developer (using new()), and also deallocated explicitly by the developer, with delete().

The object can be accessed by other parts of the application as long as its location in memory is known: the variable that contains the location is a pointer. If a programmer knows the size of an object, he can do fancy things (such as loop through the memory by adding the size of the object to the pointer) to get at the location of the next object.

However, once the pointer variable is lost, there's no longer a certain way of getting at the location of the object, and there's no way to delete the object—the memory will remain occupied for as long as the application runs, and there's no way it can be useful! This is called a memory leak, and is undoubtedly a bad thing.

One of the strengths of Python is that the programmer is freed of the responsibility 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:

Example 9-1. refs.py - showing object references

#
# 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:

<__main__.theClass instance at 0x81d9cb4>
[<__main__.theClass instance at 0x81d9cb4>]
{'key': <__main__.theClass instance at 0x81d9cb4>}

The object instance (0x81dcb4 is the object's 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.

Prev | Home | Next
Unicode strings | Up | Circular references


Qt Class Hierarchy

Prev Next

Chapter 10. Qt Class Hierarchy

Table of Contents
Hierarchy
Base classes
Application classes
Widget foundations: QWidget
Basic widgets
Advanced widgets
Layout managers
Dialogs and Standard Dialogs
Qt Utility classes and their Python equivalents

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.

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 the Section called Object and class introspection in Chapter 9.

Figure 10-1. Qt Inheritance Hierarchy (only the most important classes)

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.

In addition to the inheritance hierarchy, there is an ownership hierarchy, where a window will own, for instance, toolbars, a menubar and a statusbar. If the window is deleted, all child objects will be deleted, too, and if a keypress event arrives for an application, it will traverse the tree until it arrives at the right spot. The ownership hierarchy comes into existence by creating objects with their owner object as parent - see the Section called References and ownership in Chapter 9 about this principle.

Figure 10-2. Object Ownership Hierarchy

Prev | Home | Next
Object and class introspection | Up | Base classes


Qt Designer, BlackAdder and uic

Prev Next

Chapter 11. Qt Designer, BlackAdder and uic

Table of Contents
Introduction
Advanced Designer topics

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 BlackAdder's 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 menu's, 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++

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 application.

Starting out with the designer module

Beginning this process is easy, at least as far as GUI design is concerned! After choosing New from the File menu, you will be presented with a dialog that asks you to choose what kind of item you want to create:

Selecting a template for a new GUI design.

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 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.

The Dialog template, which appears rather bare.

Wizard is more interesting. This template, based on QWizard, offers everything you need to create these popular "hand-holding" forms.

The wizard template.

Configuration Dialog is interesting, too. It is meant for application-wide preference dialogs, with a listbox containing configuration categories on the left, and a new set of tabbed forms for each configuration category on the right. Note that you can just as easily put pixmaps in a listbox as text strings. It is far more professional to give the user icons to select from instead of text labels in a listbox.

The Configuration dialog template.

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 dialog-with-the-buttons-on-the-right template.

The last default template is for creating a bottom-buttoned dialog with a tab strip on top.

The tabbed dialog template.

Creating a design

I'm assuming that you are familiar with the concept of drawing a gui on a grid. Click on the icon that represents the widget you want, click on the grid, drag it to where you want, and alter any properties you want. It's as simple as that—I can't make it any more difficult for you.

A partially filled-in form.

Grouping widgets

One thing to keep in mind is the essential difference between container widgets and normal widgets. Container widgets can hold other widgets in a parent-child relation. 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.

A groupbox holding radio buttons.

Layout management

A layoutmanager is a container, too, but here the procedure is just the other way around. You first create the widgets. Then, you select all widgets that should be managed, and then select one of the layout managers (horizontal, vertical or grid). Every time you add a new widget to the container (or a spacer object) you will break the layout and have to recreate it. You can also nest layout managers, to create more complicated effects.

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.

Tab order and accelerators

A good GUI allows the user to do everything with just the keyboard. For this, it is necessary to give every control its own accelerator key. There are two possibilities for creating these accelerators. Either the widget has a label component of its own, in which case typing an & before the letter you want to make the accelerator will suffice. Or, and this is more usual, the widget is purely graphical, but can be associated with a QLabel. Again, the & defines the accelerator, but you must still somehow link the label with the widgets. This is done through the buddy option in the properties sheet. If you select the label, and then enter the name of the associated widget in the buddy field, a link will be made.

Selecting a buddy.

BlackAdder can check for duplicate accelerators. In the Edit menu, select the option Check Accelerators. There's 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!

Setting the tab order.

Setting the tab order right now becomes one of those pleasurable little tasks that give a developer a bit of thinking time.

Prev | Home | Next
Qt Utility classes and their Python equivalents | Up | Advanced Designer topics


Application Frameworks

Prev Next

Chapter 12. Application Frameworks

Table of Contents
Architecture: models, documents and views
Macro languages
Project layout

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 don't 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, I'll show you one useful way of laying out the project structure. In the next few chapters we'll build this framework into a real application.

Architecture: models, documents and views

Let's start with the basics. Applications are, essentially, interfaces that manipulate data. Whether you are handling records from a database, an object tree from a parsed HTML page, a game world, or a stream from a network socket, there is always a reality that is mediated to the user with an interface.

From this, it follows naturally that it would be a good idea to separate bits of software that handle the data from the interface. After all, you might want to manipulate the same data with a different interface. In some development environments this is quite difficult to achieve. Visual Basic, for instance, almost mandates that you entangle your data-mangling code with your GUI code. On the other side of the scale, SmallTalk has explicit support for the most extended form of the celebrated Observer pattern — with the Model/View/Controller framework for the SmallTalk user interface (or, in later versions, the Model-View-Presenter architecture).

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, doesn't 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 doesn't 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. That's why Java's 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, 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.

A document-view framework

The most basic architecture in which application model and interface are separated is the document-view paradigm. Here, you have two basic modules: one representing your data (the document), and one representing the interface that shows the data to the user (the view). This architecture is prevalent in the Windows world: the entire Microsoft MFC library is based on its principles, and it is also popular in the Unix world, where many KDE applications are based on it.

The document-view architecture

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, we'll work with the framework to build a real application. We'll 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.

Example 12-1. A simple document-view framework

class DocviewDoc(QObject):

    def __init__(self, *args):
        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. That's 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 the Section called QColor in Chapter 10, 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):
    def __init__(self, doc, *args):
        apply(QWidget.__init__, (self, ) + args)
        self.doc = doc
        self.connect(self.doc, PYSIGNAL("sigDocModified"),
                     self.slotDocModified)
        self.slotDocModified(self.doc.isModified())

    def slotDocModified(self, value):
        if value:
            self.setBackgroundColor(QColor("red"))
        else:
            self.setBackgroundColor(QColor("green"))

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()

The controller keeps a dictionary of actions, making it easier to refer to those actions when populating the menu and toolbars. The dictionary can also be used to export functionality for a macro language, by calling the QAction.activated() slot, which is connected to the relevant slots in the controller. The pixmap is in the form of an inline XPM image, which is not shown here.

    def initActions(self):
        fileQuitIcon=QIconSet(QPixmap(filequit))
        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 menu's 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)

    def initStatusBar(self):
        self.statusBar().message("Ready...")

Here the document, or application model, is initialized.

    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 we're 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 really 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.

    #
    # 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 doesn't 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:

A very simple document-view framework application

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 mouseDoubleClickEvent(self, ev):
        self.doc.slotModify()

This bypasses the controlling application (DocviewApp) and leads to an uncomfortably tight coupling between view and document. Another way to notify the document of the double-click is to let the view emit a signal, which can be caught by the application object and connected to the document slot. Replace the previous function with the following function in the DocviewView class instead:

    def mouseDoubleClickEvent(self, ev):
        self.emit(PYSIGNAL("sigViewDoubleClick"),())

And to the DocviewApp:

    def initView(self):
        self.view = DocviewView( self.doc, self)
        self.setCentralWidget(self.view)
        self.connect(self.view, PYSIGNAL("sigViewDoubleClick"),
                     self.slotEditDoc)

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.

Prev | Home | Next
Creating real applications with PyQt | Up | Macro languages


Actions: menus, toolbars and accelerators

Prev Next

Chapter 13. Actions: menus, toolbars and accelerators

Table of Contents
Actions
Menus
Toolbars
Keyboard accelerators
Setting an application icon

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. Let's take a deeper look at the possibilities of actions, toolbars and menus.

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 action—such as redo, when there's nothing to redo—and you're 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:

  • Pull-down menus text

  • Tooltip text

  • What's This text

  • 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:

Example 13-1. Defining a complex toggle action

    planAction=QAction(self)                                 (1)
    planAction.setIconSet(QIconSet(QPixmap(plan)))           (2)
    planAction.setText("Plan")                               (3)
    planAction.setMenuText("&Plan ...")                      (4)
    planAction.setOn(0)
    planAction.setStatusTip("Enable the cunning plan")       (6)
    planAction.setToolTip("Enables the cunning plan")        (7)
    planAction.setWhatsThis(                                 (8)
"""Plan

Selecting plan enables the cunning plan
for this window.""")
    planAction.setAccel(QAccel.stringToKey("CTRL+C"),)       (9)
    planAction.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 icon—or 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 hasn't 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 wouldn't want the shortcut to show up on toolbar captions—they don't work there, so you can set them in the menu text, by prefixing an ampersand (&) to the shortcut letter.
Sometimes, a certain action toggles an application state on or off. Examples include setting toolbar captions of toolbar icon sizes, or bold or normal fonts. By grouping actions in action groups, you can create mutually exclusive groups (such as centered, justified or ragged text in a text editor).
This text appears in the statusbar of the mainwindow when the user presses but doesn't release the menu option or toolbar button associated with the action.
This is a longer text that appears in the form of a yellow note when the user presses the optional ‘what's this' button, which appears on the toolbar.
The keyboard accelerator that activates this action—for instance, pressing the control and the s key together—will trigger the activated() signal, which might be connected to the save() slot function. It is easiest to construct this using QAccel's stringToKey() function. Not only is this very convenient, it can also be translated to languages where a different shortcut key is preferred, by using either pygettext or PyQt's tr(). See Chapter 25 for more information on internationalizing an application.
Of course, if you create a toggle action, it is nice to be able to set the initial state—which is what this does.

The QAction class can emit two signals:

  • 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.

        self.connect(planaction,
                     SIGNAL("activated()"),
                     self.slotExecuteCunningPlan)

or, for a toggle action:

        self.connect(planaction,
                     SIGNAL("toggled(bool)"),
                     self.slotActivateCunningPlan)

All that remains, is to add the actions to pulldown menus or toolbars:

        self.planaction.addTo(self.planMenu)
        self.planaction.addTo(self.toolBar)

Prev | Home | Next
Project layout | Up | Menus


Automatic testing with PyUnit

Prev Next

Chapter 14. Automatic testing with PyUnit

Table of Contents
About unittests
Starting out
A first testcase
Collecting tests in a test suite
A more complicated test
Large projects
Testing signals and slots
Conclusion

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.

About unittests

Have you ever done maintenance on a large application and broken something because you changed something, somewhere? Or worse, only noticed the breakage when a user mailed you? Have you ever begun writing an application, but were unable to complete it because the whole castle of cards collapsed due to excessive fragility?

It has probably happened to you, and it certainly has happened to me. Testing software is a boring chore, and besides, everything has to be finished yesterday, or today at the latest. However, not testing will cost you a lot of time, too, and it's more fun to program than to bug-hunt. It would be best if automated testing could be made part of the edit-compile-crash cycle.

This has occurred before to a lot of people, but the honor of ‘inventing' automatic unit-testing belongs to Erich Gamma and Kent Beck - familiar names to developers everywhere. They started writing unit-test frameworks for SmallTalk, moving on to Java and other languages.

The idea is simple but ingenuous: first the developer writes his test, then the class that will make the test work; the process is repeated until the created code fits the application you're developing. All the while, you will get instant feedback from a small GUI app that runs your tests and shows you a green progressbar when 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 isn't as much fun.

All is well - the bar is green!

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 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.

It's 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...

Prev | Home | Next
Setting an application icon | Up | Starting out


A More Complex Framework: Multiple Documents, Multiple Views

Prev Next

Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views

Table of Contents
Introduction
Document/View Manager
The Document Manager
Document
View
The actual application
Conclusion

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. It's a pity that in PyQt 2 dockable windows are not supported — for that you need to use QDockWindow in PyQt 3.

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.

The Gimp littering my desktop

Finally, there is the style made popular by IDE's 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' IDE's: 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.

KDevelop — the epitome of an interface with tabs.

I have chosen to disregard the more exotic styles of multiple-document interfaces. There are many varieties of these, such as vi's, in which you have a set of documents you can visit one after another (but not go back) and Emacs, which gives you a set of buffers that may or may not be presented in another window or another frame (where window and frame are the exact opposite of what you'd expect). These oddities should remain just that — oddities.

As an aside, it is interesting to note that all these styles originated with programming environments. It is often the case that a programming environment influences the developer in his interface decisions — and this might very well fail to support the needs of the user...

In this chapter, we first develop a document/view manager that can be used to manage a situation in which you have more than one document with more than one view. It will also handle keeping track of modifications to the documents, saving, 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.

Prev | Home | Next
Conclusion | Up | Document/View Manager


User Interface Paradigms

Prev Next

Chapter 16. User Interface Paradigms

Table of Contents
Tabbed documents
Back to the MDI windows
A row of split windows
A stack of documents
A more complex view management solution
Conclusion

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.

Tabbed documents

Like most user interface paradigms, the tabbed document paradigm has been popularized by current integrated development environments. A tabbed document collects all open documents in one window, with a row of tabs to facilitate easy navigation of documents. This paradigm has become so prevalent that even the old stalwart of user interface conservatism, XEmacs, supports it.

It turns out to be remarkably easy to implement a tabbed document interface. First, let's determine what we want to get out of this component. It is the first of several generic components that can take views — i.e. QWidget's— and show them in an application workspace. All view managers should have the same API. That allows the user to choose his favorite way of working without giving us lots of work — because, from the point of view of the application, all view managers are exactly the same.

We will provisionally call the component that manages tabbed views TabManager. The TabManager is meant to be almost a drop-in replacement for the QWorkspace we used in the Chapter 15. Therefore, it should support most of the same functionality: adding, removing and listing actual views. Other capabilities of QWorkspace don't make sense: you cannot tile or cascade tabbed windows. There must be some way to indicate to the wrapping application whether the view manager supports these capabilities.

PyQt offers a QTabWidget, which fits the basics of our needs perfectly. However, in contrast with the QWorkspace, where merely creating a widget with the workspace as parent widget was enough to let it be managed, QTabWidget wants us to explicitly add pages, and thus widgets, to its list of tabs. Finally, it also allows the addition and removal of pages. We can also request a reference to the active view, and ask to be notified of page changes.

QTabWidget is used in the QTabDialog dialog window class, and makes use of QWidgetStack and QTabBar. QWidgetStack keeps a stack of widgets of which only one is shown at a time. QTabBar, which keeps a row of tabs. Tabs can be square or triangular (the latter is seldom seen nowadays, for it is very ugly), and shown on the top or bottom of the window.

Applications that handle documents that consist of several (but not many) pages often show a row of triangular tabs at the bottom of the window. You cannot set the tabs to appear at the side of the window. That's a pity, since it is a position that is quite often preferred by users.

Let us take a look at the implementation of a tabbed document manager:

"""
tabmanager.py - tabbed document manager for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class TabManager(QTabWidget):

    def __init__(self, *args):
        apply(QTabWidget.__init__,(self, ) + args)
        self.views=[]
        self.setMargin(10)

The TabManager is derived from QTabWidget. A simple python list of views is kept, otherwise we would not be able to retrieve a list of all open views for the ‘windows' menu. The margin between tab and document should really be a user-settable property, but we won't develop a user preferences framework until chapter Chapter 18.

    def addView(self, view):
        if view not in self.views:
            self.views.append(view)
            self.addTab(view, view.caption())
            self.showPage(view)

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 won't manage the layout of the page. This means that when you create a new window and resize the application window, the contents won't resize with it. Simply drawing the tab widget's attention to the page will suffice, however.

With PyQt's QWorkspace it was enough to create a widget with the workspace as its parent—the 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:

    def _createView(self, document, viewClass):
        view = viewClass(self._viewManager,
                         document,
                         None,
                         QWidget.WDestructiveClose)
        if self._docToViewMap.has_key(document):
            index = len(self._docToViewMap[document]) + 1
        else:
            index = 1
        view.setCaption(document.title() + " %s" % index)

        self._viewManager.addView(view)

        view.installEventFilter(self._parent)

        if self._viewToDocMap == {}:
            view.showMaximized()
        else:
            view.show()

        return view

To return to the TabManager class:

    def removeView(self, view):
        if view in self.views:
            self.views.remove(view)
            self.removePage(view)

    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, it's a simple change:

    def _removeView(self, view, document):
        try:
            self._docToViewMap[document].remove(view)
            self._viewManager.removeView(view)
            del self._viewToDocMap[view]
        except ValueError, e:
            pass # apparently already deleted

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 cascade(self): pass

    def tile(self): pass

    def canCascade(self):
        return FALSE

    def canTile(self):
        return FALSE

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.

Prev | Home | Next
Conclusion | Up | Back to the MDI windows


Creating Application Functionality

Prev Next

Chapter 17. Creating Application Functionality

Table of Contents
Introduction
The view
The document
Saving and loading documents
Undo, redo and other editing functions
Conclusion

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 isn't 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.

Giving the project a name

Before starting out, we should decide upon a name for this project. I rather like kalam — a word which means pen or pencil, and which is derived from the Latin calamus. I have snapshotted the version we have at the end of each chapter so you can follow the development of this project. I didn't print the complete code of every class in every chapter, as that would be too tedious for words!

Prev | Home | Next
Conclusion | Up | The view


Application Configuration

Prev Next

Chapter 18. Application Configuration

Table of Contents
Platform differences
The Python way of handling configuration settings
Implementing configurations settings for Kalam
Settings in Qt 3.0
Conclusion

Every user has a preferred way of doing things — and a good application should be accommodating enough that important choices—such 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.

Platform differences

On a Windows system, most applications store user preferences in a central database, called the Registry. This book is not the place to argue about the wisdom or folly of keeping all application and system settings in one database, which can only be accessed with specialized tools. Most modern windows applications no longer use the once prevalent .ini standard. Using .ini, an application could store settings in a file in either the installation directory, the windows directory, or in one of two configuration files: win.ini or system.ini. Windows has only recently become a multi-user system, so it is still difficult to determine where to store user specific settings. In Windows 2000, I suggest the C:\Documents and Settings\{Username}\Local Settings\Application Data directory. In the registry, there is the HKEY_USER branch.

The Unix standard is not so much a standard as a gentle guide. You can store system-wide application settings in the /etc directory, or one of its subdirectories, or in the /usr/share directory together with resources... Or in /usr/local/share — or in any number of other places.

User settings on a Unix system are generally stored in so-called dot files, or dot directories. These are files or directories that start with a dot (.) and are thus ‘invisible' when the user asks for the contents of a directory using ls (or browses the directory with a filemanager like konqueror). The dot-files or dot-directories are located in the home directory of the user. A Unix home directory can be compared to the user's directory under C:\Documents And Settings in Windows 2000. It is generally found under /home. You can retrieve the location of this directory with:

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.

Prev | Home | Next
Conclusion | Up | The Python way of handling configuration settings


Using Dialog Windows

Prev Next

Chapter 19. Using Dialog Windows

Table of Contents
Modal: a preferences dialog
Non-modal: Search and replace
Conclusion

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.

Modal: a preferences dialog

We will start with a preferences dialog. Nowadays, the taste is for dialogs with a strip of icons to the left that somehow indicates what section should be shown. But we will start out with a simple tabbed dialog that PyQt supports out of the box, and for which we don't have to draw icons (that's always the difficult bit, creating the artwork).

Designing the dialog

So: time to fire up the designer module of BlackAdder or Qt Designer!

The settings dialog - editor tab

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 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.

The settings dialog - interface tab

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 doesn't need a preview.

To fill in the preview I've 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.

The settings dialog - document tab

As for the document, we need two settings: the first is the document encoding. 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-dressing—actually 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 17—ultimately, 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 development—and it often feels that way indeed.

Creating the settings dialog window

The first part of the drill is well known: compile the frmsettings.ui file to Python using pyuic.

pyuic -x frmsettings.ui > frmsettings.py

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

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
import os, sys
from qt import *
import kalamconfig

from frmsettings import FrmSettings

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()

The DlgSettings dialog is a subclass of FrmSettings, which we created with Designer. In the constructor we create four objects for housekeeping purposes, to store changed settings until the user chooses to apply them by pressing OK, or to cancel.

These objects represent the editor font, the editor text color, the editor background color and the background color of the MDI workspace. As you can see from the calls to kalamconfig, actually implementing this dialog necessitated quite a few changes to the kalamconfig module.

The full source of kalamconfig is not so interesting for this chapter, but it is available with the rest of the code. To summarize the development: all settings are now retrieved and set through a single pair of get/set functions. There are a lot more settings, too. If a setting requires special handling, then the relevant get/set function is retrieved from a dictionary (you can just as easily store references to functions or classes in a dictionary as in strings, since everything is considered an object) and executed with apply(). If a setting is changed, a signal is emitted from the QApplication instance, which can be reached with the global variable qApp. Note how the actual signal identifier is constructed dynamically:

#
# kalamconfig.py
# Get and set - set emits a signal via Config.notifier
#
customGetSetDictionary = {
    "style" : (getStyle, setStyle),
    "workspace" : (getWorkspace, setWorkspace),
    "textfont" : (getTextFont, setTextFont),
    "textforeground" : (getTextForegroundColor, setTextForegroundColor),
    "textbackground" : (getTextBackgroundColor, setTextBackgroundColor),
    "mdibackground" : (getMDIBackgroundColor, setMDIBackgroundColor),
}

def set(attribute, value):
    if customGetSetDictionary.has_key(attribute):
        apply(customGetSetDictionary[attribute][1], (value,))
    else:
        setattr(Config, attribute, value)
    qApp.emit(PYSIGNAL("sig" + str(attribute) + "Changed"),
                         (value,))

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)

        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:

  • Background - general background color.

  • Foreground - general foreground color.

  • Base - background color text entry widgets

  • Text - the foreground color used with Base.

  • Button - general button background color

  • ButtonText - for button texts

  • Light - lighter than Button color.

  • Midlight - between Button and Light.

  • Dark - darker than Button.

  • Mid - between Button and Dark.

  • Shadow - a very dark color.

  • Highlight - a color to indicate a selected or highlighted item.

  • HighlightedText - a text color that contrasts to Highlight.

All colors are normally calculated from the Background color. Setting the background color of the editor with the convenience function setBackgroundColor() won't 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 isn't 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 Qt's highly optimized drawing engine becomes confused. This will be done in the slot function that's 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)

The preview for the interface style is initialized in initWindowViewCombo. Note that QLabel is rather more simple in its needs than QMultiLineEdit as regards colors. Here, you can just use the convenience function setBackgroundColor (setEraseColor() in Qt 3) to show the preview color for the MDI workspace.

    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):
        self.cmbStyle.clear()
        styles = kalamconfig.stylesDictionary.keys()
        styles.sort()
        try:
            currentIndex = 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.

# kalamconfig.py - styles dictionary
stylesDictionary = {
    "Mac OS 8.5" : QPlatinumStyle,
    "Windows 98" : QWindowsStyle,
    "Motif" : QMotifStyle,
    "Motif+" : QMotifPlusStyle,
    "CDE" : QCDEStyle
    }

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 doesn't return a list. If we'd written:

        styles = kalamconfig.stylesDictionary.keys().sort()

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 setStyle(self, style):
        kalamconfig.set("style", str(style))
        qApp.setStyle(kalamconfig.get("style")())

Changing a style is instantaneous in Kalam, if only because it is fun to run through all the styles and see the application changing under your fingers. Therefore, we immediately update the style setting, and call qApp.setStyle() to propagate the changes to the application widgets.

    def initWindowViewCombo(self):
        self.cmbWindowView.clear()

        workspaces = kalamconfig.workspacesDictionary.keys()
        workspaces.sort()
        try:
            currentIndex = 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)

Setting up the workspace selection combobox is similar to setting up the styles combobox. The only interesting point is the connection to setWorkspacePreview. This function updates the small image that shows what each option means. These images were made from snapshots, and scaled down with Pixie, a KDE graphics application (which is now obsolete).

    def setWorkspacePreview(self, workspace):
        workspace = str(workspace) + ".png"
        # XXX - when making installable, fix this path
        pixmap = QPixmap(os.path.join("./pixmaps",
                                      workspace))
        self.pxViewSample.setPixmap(pixmap)

As you can see, application development is messy, and I don't 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:
            currentIndex = 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:

# kalamconfig.py - encodings dictionary

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",
    "Turkish (iso 8859-9)": "iso-8859-9",
    "Inuit (iso 8859-10)": "iso-8859-10",
    "Thai (iso 8859-11)": "iso-8859-11",
    "Baltic (iso 8859-13)": "iso-8859-13",
    "Gaeilic, Welsh (iso 8859-14)": "iso-8859-14",
    "iso 8859-15": "iso-8859-15",
    "Cyrillic (koi-8)": "koi8_r",
    "Korean (euc-kr)": "euc_kr"}

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)
            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 tuple—and if ok is true, then we update the font of the preview and also set the textFont variable to reflect the users choice.

Finally, there's 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()

Calling the settings dialog window

In order to be able to call the dialog window, we must first create a new QAction and add it to a likely menu. This is done in KalamApp:

# 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)
        ...

The settingsSettings is connected to a new slot in KalamApp:

    # Settings slots

    def slotSettingsSettings(self):
        dlg = DlgSettings(self,
                          "Settings",
                          TRUE,
                          Qt.WStyle_Dialog)

The dialog window is constructed as a function-local variable. That means that if the function reaches its end, the dlg object is deleted. A settings dialog is typically modal. Whether a dialog is created modal or non-modal is determined in the constructor. The first argument to DlgSettings.__init__() is the parent window, in this case KalamApp. The second argument is a name. The third argument determines whether the dialog is modal—TRUE means modal, FALSE means non-modal. FALSE is also the default. The last argument can be any combination of widget flags. For a dialog box, Qt.WStyle_Dialog seems rather appropriate. Note that in Qt 3, this flag is renamed to Qt.WType_Dialog There are a whole lot of flags (the following list is based on Qt 2 - there have been some changes):

  • WType_TopLevel - a toplevel window

  • WType_Modal - Makes the widget modal and inplies WStyle_Dialog.

  • WType_Popup - this widget is a popup top-level window, it is modal, but has a window system frame appropriate for popup menus.

  • WType_Desktop - This widget is the desktop - you can actually use PyQt to paint on you desktop.

  • WStyle_NormalBorder - The window has a normal border.

  • WStyle_DialogBorder - A thin dialog (if you windowmanager on X11 supports that).

  • WStyle_NoBorder - gives a borderless window. However, it is better to use WStyle_NoBorderEx instead, because this flag will make the window completely unusable on X11.

  • WStyle_NoBorderEx - gives a borderless window.

  • WStyle_Title - The window jas a title bar.

  • WStyle_SysMenu - adds a window system menu.

  • WStyle_Minimize - adds a minimize button. On Windows this must be combined with WStyle_SysMenu to work.

  • WStyle_Maximize - adds a maximize button. See WStyle_Minimize.

  • WStyle_MinMax - is equal to WStyle_Minimize|WStyle_Maximize. On Windows this must be combined with WStyle_SysMenu to work.

  • WStyle_ContextHelp - adds a context help button to dialogs.

  • WStyle_Tool - A tool window is a small window that contains tools (for instance, drawing tools, or the step buttons of a debugger). The tool window will always be kept on top of its parent, if there is one.

  • WStyle_StaysOnTop - the window should stay on top of all other windows.

  • WStyle_Dialog - indicates that the window is a dialog window. The window will not get its own taskbar entry and be kept on top of its parent by the window system. This is the flag QDialog uses, and it is not necessary for us to explicitly pass it to DlgSettings.

  • WDestructiveClose - makes Qt delete this object when the object has accepted closeEvent(). Don't use this for dialog windows, or your application will crash.

  • WPaintDesktop - gives this widget paint events for the desktop.

  • WPaintUnclipped - makes all painters operating on this widget unclipped. Children of this widget, or other widgets in front of it, do not clip the area the painter can paint on.

  • WPaintClever - indicates that Qt should not try to optimize repainting for the widget, but instead pass the window system repaint events directly on to the widget.

  • WResizeNoErase - indicates that resizing the widget should not erase it. This allows smart-repainting to avoid flicker.

  • WMouseNoMask - indicates that even if the widget has a mask, it wants mouse events for its entire rectangle.

  • WNorthWestGravity - indicates that the widget contents are north-west aligned and static. On resize, such a widget will receive paint events only for the newly visible part of itself.

  • WRepaintNoErase - indicates that the widget paints all its pixels. Updating, scrolling and focus changes should therefore not erase the widget. This allows smart-repainting to avoid flicker.

  • WGroupLeader - makes this widget or window a group leader. Modality of secondary windows only affects windows within the same group.

You can combine these flags with the or (or |) operator.

Showing a modal dialog is a matter of simply calling exec_loop():

        dlg.exec_loop()
        if dlg.result() == QDialog.Accepted:
            kalamconfig.set("textfont", dlg.textFont)
            kalamconfig.set("workspace", str(dlg.cmbWindowView.currentText()))
            kalamconfig.set("style", str(dlg.cmbStyle.currentText()))
            kalamconfig.set("textbackground", dlg.textBackgroundColor)
            kalamconfig.set("textforeground", dlg.textForegroundColor)
            kalamconfig.set("mdibackground", dlg.MDIBackgroundColor)
            kalamconfig.set("wrapmode", dlg.cmbLineWrapping.currentItem())
            kalamconfig.set("linewidth", int(str(dlg.spinLineWidth.text())))
            kalamconfig.set("encoding", str(dlg.cmbEncoding.currentText()))
            kalamconfig.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,
                     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):

    def __init__(self, parent, doc, *args):
        ...
        self.editor.setFont(kalamconfig.get("textfont"))
        self.setWordWrap(kalamconfig.get("wrapmode"))
        self.setBackgroundColor(kalamconfig.get("textbackground"))
        self.setTextColor(kalamconfig.get("textforeground"))
        ...
        self.connect(qApp,
                     PYSIGNAL("siglinewidthChanged"),
                     self.editor.setWrapColumnOrWidth)
        self.connect(qApp,
                     PYSIGNAL("sigwrapmodeChanged"),
                     self.setWordWrap)
        self.connect(qApp,
                     PYSIGNAL("sigtextfontChanged"),
                     self.editor.setFont)
        self.connect(qApp,
                     PYSIGNAL("sigtextforegroundChanged"),
                     self.setTextColor)
        self.connect(qApp,
                     PYSIGNAL("sigtextbackgroundChanged"),
                     self.setBackgroundColor)
        ...

    def setTextColor(self, qcolor):
        pl = self.editor.palette()
        pl.setColor(QColorGroup.Text, qcolor)
        self.editor.repaint(TRUE)

    def setBackgroundColor(self, qcolor):
        pl = self.editor.palette()
        pl.setColor(QColorGroup.Base, qcolor)
        self.editor.setBackgroundColor(qcolor)
        self.editor.repaint(TRUE)

    def setWordWrap(self, wrapmode):

        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 interface style " +
                                        "will only be activated when you " +
                                        "restart the application.")
        ...

Prev | Home | Next
Conclusion | Up | Non-modal: Search and replace


A Macro Language for Kalam

Prev Next

Chapter 20. A Macro Language for Kalam

Table of Contents
Executing Python code from Python
Integrating macros with a GUI
Creating a macro API from an application
Conclusion

One thing that separates a run-of-the-mill application from a real tool—one that users will take refuge in day after day—is 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! Isn't 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 it's well worth the effort.

Executing Python code from Python

There are three basic ways to execute Python code that is not directly (i.e. imported as a module) part of your application. Single statements can be executed with eval(); we already encountered eval() in Chapter 18. Strings that contain more than a single statement of Python code can be executed with exec(), while Python code that is saved in a file can be executed with execfile().

Both eval() and exec() can be fed either plain text strings or pre-compiled Python byte code. You can create blobs of byte code with the compile() function, but you don't have to—it is simply a little more efficient to use bytecode if you execute the same code more than once. The evalfile() function reads in a file and executes the contents, which must be plain text. You cannot feed execfile compiled Python byte code.

Please note that eval(), exec(), execfile() and compile() are the real 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 won't 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.

Let's compare these locals and globals from an interactive Python session:

Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> globals()
{'__doc__': None, '__name__': '__main__',
 '__builtins__': <module '__builtin__' (built-in)>}
>>> def f():
...     a=1
...     print "locals: ", locals()
...
>>> globals()
{'f': <function f at 0x8124a94>, '__doc__': None,
 '__name__': '__main__',
 '__builtins__': <module '__builtin__' (built-in)>}
>>> f()
locals:  {'a': 1}
>>>

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.

Playing with eval()

eval() functions as if it executes a single line of code in the Python interpreter: it returns a value that represents the result of the evaluated expression. If the statement you give to eval() raises an exception, the surrounding code gets that exception, too. Playing around with eval() will give you a feeling for what it can do for you.

Figure 20-1. Playing with eval()

boud@calcifer:~/doc/pyqt/ch15 > python
Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> eval
<built-in function eval>
>>> eval("1==1")
1
>>> import string
>>> eval("string.split('bla bla bla')")
['bla', 'bla', 'bla']
>>> eval("string.split('bla bla bla')", {}, {})
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 0, in ?
NameError: There is no variable named 'string'
>>> eval("""from qt import *
... s=QString("bla bla bla")
... print str(s).split()
... """)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 1
    from qt import *
       ^
SyntaxError: invalid syntax
>>>

First, we take a look at what "eval" is for a beast. A built-in function. OK, let's 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 isn't known anymore. Trying to evaluate something more complicated, something that is not a single expression that returns a value (even if it's only None) doesn't work at all - which is why exec() exists.

Playing with exec

First, exec is really a statement, not a function, so it doesn't return anything. Just as with eval(), exceptions are propagated outside the code block you execute. You can feed exec a string, a compiled code object or an open file. The file will be parsed until an EOF (end-of-file) occurs, and executed. The same rules hold for the global and local namespace dictionaries as with eval() - but keep in mind that running exec might add new items to those namespaces.

Figure 20-2. Playing with exec

boud@calcifer:~/doc/pyqt/ch15 > python
Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> globals()
{'__doc__': None, '__name__': '__main__',
 '__builtins__': <module '__builtin__' (built-in)>}
>>> code = """
... import qt
... s = qt.QString("bla bla bla")
... print string.split(str(s))
... """
>>> exec code
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 4, in ?
NameError: There is no variable named 'string'
>>> import string
>>> exec code
['bla', 'bla', 'bla']
>>> globals()
{'__doc__': None, 'string': <module 'string' from
'/usr/lib/python2.0/string.pyc'>, '__name__':
 '__main__', '__builtins__': <module '__builtin__'
 (built-in)>, 'qt': <module 'qt' from
'/usr/lib/python2.0/site-packages/qt.py'>,
 'code': '\012import qt\012s = qt.QString("bla bla bla")\012print
string.split(str(s))\012', 's': <qt.QString instance at 0x8278af4>}
>>>

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 doesn't work: it throws a NameError because string isn't 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.

Playing with execfile()

The execfile() statement is rarely used; after all, it can't do anything beyond what the plain exec statement already does. execfile() functions exactly the same as exec, except that the first argument must be a filename (it doesn't need to be an open file object). Note that execfile() differs from import in that it doesn't create a new module in the global namespace. Note the difference between execfile() and import in the following output:

Figure 20-3. Playing with execfile()

Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> execfile("main.py")
Initializing configuration
{'kalam': <kalamapp.KalamApp instance at 0x825f014>,
 'app': <qt.QApplication instance at 0x814a2a4>, 'args': ['']}
Saving configuration
>>> import main
>>> globals()
{..., 'main': <module 'main' from 'main.py'>, ... }

In the middle of all the qt classes the main module of Kalam imports into globals, we find the main module itself, which isn't there if we just execfile main.py.

Prev | Home | Next
Conclusion | Up | Integrating macros with a GUI


Drawing on Painters and Canvases

Prev Next

Chapter 21. Drawing on Painters and Canvases

Table of Contents
Working with painters and paint devices
QCanvas
Conclusion

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 cope—even on the most modern machines. In such cases, it is quite easy to replace the Python class with a Qt-based C++ object (you won't have to translate your whole Python application). See Appendix C for information on the wrapping of new C++ objects with sip.

Working with painters and paint devices

In Chapter 10 we introduced QPainter, for creating our little scribbling example application, event1.py. In this section we will take a closer look at the organization and use of the PyQt painting mechanism.

Painting in PyQt involves the cooperation of two classes: a QPainter and a QPaintDevice. The first is a very efficient abstraction of various drawing 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... We'll discuss the basics of printing in Chapter 24.

A painting example

There is little we can do using QPainter and QPaintDevices in our Kalam project — but after long and hard thinking I thought a rolling chart that counts how many characters the user types per minute might be a nice, although completely useless (and possibly frustrating) except for real productivity freaks.

Example 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...

"""
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 *

FIVE_SECONDS = 1000 * 5 #  5 seconds in milli-seconds
AVERAGE_TYPESPEED = 125 # kind of calibration
BARWIDTH = 3

TRUE=1
FALSE=0

No surprises here—just some declarations. I like to work with names instead of magic numbers, and to conform to practice in other programming languages, those names are in all-caps, even though they are not constants.

class TypoGraph(QPixmap):
    """ TypoGraph is a subclass 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"))
        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 won't 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 won't 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 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 won't 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.

    def update(self, count):
        """
        Called periodically by a timer to update the count.
        """
        self.count = count

        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"))
                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 we've 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 don't use the complete range, but step directly from 100 (fairly dark) to 228 (quite bright). The step is computed by 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. There's 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):

    def __init__(self, docmanager, workspace, w, h, *args):
        apply(QWidget.__init__, (self,) + args)

        self.docmanager = docmanager
        self.workspace = workspace

        self.resize(w, h)
        self.setMinimumSize(w,h)
        self.setMaximumSize(w,h)

        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.

    def addGraph(self, document, view):
        self.currentGraph = TypoGraph(0,
                                      self.h,
                                      self.w)
        self.graphMap[document] = (self.currentGraph, 0)
        self.currentDocument = document

    def changeGraph(self, view):
        self.currentGraph = self.graphMap[view.document()][0]
        self.currentDocument = view.document()
        bitBlt(self, 0, 0,
               self.currentGraph,
               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)

        delta = newCount - prevCount

        if delta < 0: delta = 0 # no negative productivity

        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 Python's 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.

    def paintEvent(self, ev):
        p = QPainter(self)
        bitBlt(self, 0, 0,
               self.currentGraph,
               0, 0,
               self.w,
               self.h)

class TestWidget(QWidget):

    def __init__(self, *args):
        apply(QWidget.__init__, (self,) + args)
        self.setGeometry(10, 10, 50, 250)
        self.pixmap = TypoGraph(0, self.width(), self.height())
        self.timer = self.startTimer(100)

    def paintEvent(self, ev):
        bitBlt(self, 0, 0, self.pixmap, 0, 0, self.width(), self.height())

    def timerEvent(self, ev):
        self.pixmap.update(whrandom.randrange(0, 300))
        bitBlt(self, 0, 0, self.pixmap, 0, 0, self.width(), self.height())

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 it's 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:

    def slotSettingsTypometer(self, toggle):
        if toggle:
            self.typowindow = TypoMeter(self.docManager,
                                        self.workspace,
                                        100,
                                        100,
                                        self,
                                        "type-o-meter",
                                        Qt.WType_TopLevel or Qt.WDestructiveClose)
            self.typowindow.setCaption("Type-o-meter")
            self.typowindow.show()
        else:
            self.typowindow.close(TRUE)

Destroying this popup-window is important, because you don't want to waste processing power on a widget that still exists and is merely hidden. The character picker popup we will create in the next section will be hidden, not destroyed.

Prev | Home | Next
Conclusion | Up | QCanvas


Gui Design in the Baroque Age

Prev Next

Chapter 22. Gui Design in the Baroque Age

Table of Contents
Types of gui customization
Faking it with bitmaps
Creating themes with QStyle

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.

Types of gui customization

Despite the slight banter in the introduction, there are many valid reasons to design a gui for your application that diverges from the platform standard. For instance, while a word processor shouldn't look like the desk of a medieval scribe, the paste-board gui used in an application like Adobe FrameMaker is very effective, especially since many layout designers are still familiar with the glue-and-scissors approach of creating a layout. Likewise, designing an mp3-player that looks a bit like an audio rack is effective (while making it look like an angry fish-eye is less so). Generally speaking, creating an interface that matches closely the real world experience of your users is a good idea.

There is another area where a custom gui would fit well: the general redesign, or theming of the whole set of widgets, to make them more esthetically pleasing to your users. Qt already offers a selection of those themes. Your user can decide whether they want their applications to look like Motif, CDE, Silicon Graphics IRIX, Windows 95 or MacOS 9. Offering this choice means that the applications you make can always blend in unobtrusively with the other applications your users use. A toolkit like Tkinter does not offer this advantage; a user will always be aware that he or she is not working with a application native to his computing 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 that's more complicated than this.)

Prev | Home | Next
Conclusion | Up | Faking it with bitmaps


Drag and drop

Prev Next

Chapter 23. Drag and drop

Table of Contents
Handling drops
Initiating drags
Conclusion

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 Drag'n'Drop 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.

Handling drops

Our Kalam editor must be able to accepts drops from other applications, so we will have to implement this functionality.

Incoming drop operations are easy enough to handle. Basically, you call self.setAcceptDrops(TRUE) for the widget or widgets that should accept drop events, and then actually handle those drop events.

The widget that should accept the drops is our KalamView class. But setting self.setAcceptDrops(TRUE) in the constructor of KalamView won't work. This is because KalamView doesn't actually handle the drops; rather, they are handled by the QMultiLineEdit class, which is encapsulated by KalamView.

The easiest solution is to extend the small subclass of QMultiLineEdit (which we already created) to handle drop events.

Example 23-1. Handling drop events

"""
kalamview.py - the editor view component for Kalam

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE
class KalamMultiLineEdit(QMultiLineEdit):

    def __init__(self, *args):
        apply(QMultiLineEdit.__init__,(self,) + args)
        self.setAcceptDrops(TRUE)

    def dragEnterEvent(self, e):
        e.accept(QTextDrag.canDecode(e))

    def dropEvent(self, e):
        t=QString()
        if QTextDrag.decode(e, t): # fills t with decoded text
            self.insert(t)
            self.emit(SIGNAL("textChanged()"), ())

    def event(self, e):
        if e.type() == QEvent.KeyPress:
            QMultiLineEdit.keyPressEvent(self, e)
            return TRUE
        else:
            return QMultiLineEdit.event(self, e)

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 we'd 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 the operation was successful, and the result of the operation. In the case of QFontDialog, the results are returned in a tuple:

        (font, ok) = QFontDialog.getFont(self, "FontDialog")

Likewise, QTextDrag.decode wants to return two values: a boolean and a string. However, here you need to first create a QString, and pass that to QTextDrag.decode, which fills it with the text, while returning a boolean indicating whether the decoding went well.

However, having got the text by hook or by crook, we can insert it in the view, and tell the world that the text has changed (so other views on the text can update themselves).

You can now select and drag that text to a Kalam document. If you drag a file, only the filename will be inserted, because a filename can also be decoded as text. If you want to open the file instead, you still have some work to do.

Prev | Home | Next
Creating themes with QStyle | Up | Initiating drags


Printing

Prev Next

Chapter 24. Printing

Table of Contents
The QPrinter class
Adding printing to Kalam
Putting ink to paper
Conclusion

Printing: a subject that strikes fear into the heart of even the most stalwart developer—and with good reason. Getting a user's 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 work—the layout of the page for paper—is 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.

The QPrinter class

Generally speaking, there will be only one active printer attached to a user's system. This means that we will keep a single reference to a QPrinter object. Therefore, we might as well create the QPrinter in the __init__() method of the KalamApp class.

# kalamapp.py

class KalamApp(QMainWindow):
    """KalamApp is the toplevel application window of the kalam unicode editor
    application.
    """
    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:

The printer setup dialog

Prev | Home | Next
Conclusion | Up | Adding printing to Kalam


Delivering your Application

Prev Next

Chapter 26. Delivering your Application

Table of Contents
Introduction
Packaging source
Starting with distutils.
Creating Unix RPM packages
Windows installers
Desktop integration

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 McMillan's Installer, Fredrik Lundh's Squeeze (which is packaged with the PythonWorks IDE), and finally Thomas Heller's 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 McMillan's 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.

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, it's difficult to achieve even a little integration with the user's 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 menu's). 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, it's a matter of forcing your users to read the manual. I don't describe the process of packaging your own C or C++ extensions, though it is possible. Consult the Distutils manual for more information.

Prev | Home | Next
Internationalizing an Application | Up | Packaging source


Preface

Table of Contents
Who is using PyQt
For whom is this book intended
How to read this book
Conventions
Acknowledgments

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 thirty–somethings who started programming in their teens, I've 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. I've done my stretch with Oracle Forms, and served as a Java developer. On Linux, I've 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 there's always something new to learn or explore in Python, if you're in the mood, it's 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 I'd 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.

Joshua's 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 don't hide the complexity that is a part of any programming effort.

Both are right, of course, so I decided to give you both small examples and one really large one. Part I and II of this book concern themselves with concepts: here the examples are small, often amounting to less than one page of code. Part III takes you through the development of a complete, complex application. In this case an editor, but one with a lot of extra features. I think it's a very good way of learning what developing complex applications entails - I spare you none of the nasty details that software development entails.

I have tried to keep to a very clear style of coding, with few or none of the clever hacks that are possible in Python — like adding the methods of one class to another, or creating lists of function objects. The purpose is to tell you about writing real applications using Python and Qt. Clever hacking has its place, but is best savored on its own.

The emphasis of the book is also firmly on application development, not on creating graphics per se — although several techniques are mentioned here and there that have to do with creating charts and graphs.

Who is using PyQt

The combination of Python and Qt is extremely powerful, and is used in a wide variety of applications. People are scripting OpenGL applications with it, creating complex 3D models, animation applications, writing database applications, games, utilities and hardware monitoring applications. It is used in open source projects, but also by large companies, like Disney Television and Media. If you're not working on embedded software, hardware drivers or a new operating system, chances are that PyQt is the right choice for you, too.

Prev | Home | Next
GUI Programming with Python: QT Edition |  | For whom is this book intended


Reading the Qt Documentation

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Reading the Qt Documentation
GUI Programming with Python: QT Edition
Prev Next

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 didn't have space to discuss in this book. After all, this book teaches you how to use the toolkit to create complete applications, and isn't a mere duplication of the class documentation.

Fortunately, reading C++ documentation for use from Python isn't 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.

Using KDE to have quick access to the Qt documentation: If you are using KDE on Unix/X11, you can create new internet shortcuts that take you to the Qt class documentation in an instant.

KDE has a wonderful feature, called the run command window, in which you can type a short abbreviation (like "gg" for the Google search engine), followed by a colon and an argument.

If you add "file://usr/lib/qt2/doc/html/\1.html" in KControl, section Web Browsing/Enhanced Browsing:

Creating a shortcut to the Qt documentation

Then you will be able to access al Qt documentation by typing "qt:qobject", for instance. What you are typing is the name of the html document that contains the class information: all these documents have the classname as a filename, all in lowercase.

Using a shortcut to the Qt documentation

Note that the shortcut (file://usr/lib/qt2/doc/html/\1.html) should point to the place where your qt (or BlackAdder - that would work too) documentation resides. The element after the directory path (\1.html) is a simple substitution argument— \1 is replaced by what you type after the colon.

You can quickly access the run command window by pressing ALT-F2 in KDE's standard configuration.

Let's take a simple Qt class as an example: QLabel. It's 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. Don't pay attention to any spurious asterisks (*) or ampersands (&) around the arguments to the function: what matters is the type, like QWidget. Don't 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 don't need):

label=QLabel("text", None, "name")

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 private—if 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 PyQt—everything 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 don't need braces or final semicolons, of course. Or new statements. For instance:

    QLabel *label = new QLabel;
    label->setFrameStyle( QFrame::Panel | QFrame::Sunken );
    label->setText( "first line\nsecond line" );
    label->setAlignment( AlignBottom | AlignRight );

Could become in Python:

    label = new QLabel()
    label.setFrameStyle( QFrame.Panel or QFrame.Sunken )
    label.setText( "first line\nsecond line" )
    label.setAlignment( Qt.AlignBottom or Qt.AlignRight )

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 isn't 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 doesn't 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:

void valueChanged ( int value )

Can be used as:

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,

void returnPressed ( QListViewItem * )

Becomes:

    self.connect(self,
                 SIGNAL("returnPressed(QListViewItem *)"),
                 self.slotItemSelected)

The Qt documentation is not always consistent in giving signal parameters variable names - sometimes they do, sometimes they don't.

Finally, wherever there is a NULL in C++, you can use None, but you can also use None in many cases where there is a zero (0) (this being a pointer to nowhere). That means that everywhere a function takes an object (instead of a simple integer) as a parameter, you can use None. A zero is often given as a default argument in these cases, and then you don't need to give any parameter. Thus:

QListView ( QWidget * parent = 0, const char * name = 0 )

Can be called as:

listview = QListView(None, None)

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:

listview = QListView(parentWindows, "listview")

As you've seen, it's not difficult at all to translate from C++ to Python — even if you don't know any C or C++. If you do want to know more about C++, I can recommend Steven Oualline's book, Practical C++ Programming as a good beginners title.

Prev | Home | Next
Appendices | Up | PyQwt: Python Bindings for Qwt


PyQwt: Python Bindings for Qwt

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

PyQwt: Python Bindings for Qwt
GUI Programming with Python: QT Edition
Prev Next

Appendix B. PyQwt: Python Bindings for Qwt

Table of Contents
NumPy
PyQwt
Gerard Vermeulen

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 you'd 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.

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 Excel—it 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.

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

root@calcifer:/home/boud# python setup_all.py install

Once numpy is installed, you can start Python (or open the Interpreter window in BlackAdder) and import the NumPy extension:

[packer@slow packer]$ python
Python 2.1.1 (#1, Aug 20 2001, 08:17:33)
[GCC 2.95.3 19991030 (prerelease)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> from Numeric import *
>>>

A NumPy array looks like a list and can be created from a list (in fact, from any sequency type: list, tuple or string).

Let's create and print a 1-dimensional NumPy array of Python floats:

>>> a = array([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:

>>> b = array([[0.0, 1.0], [2.0, 3.0]])
>>> print b
[[ 0.  1.]
 [ 2.  3.]]
>>>

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 = array([[[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:

>>> ax = arange(0.0, 5.0, 0.5)
>>> print ax
[ 0.   0.5  1.   1.5  2.   2.5  3.   3.5  4.   4.5]
>>>

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:

def lrange(start, stop, step):
    start, stop, step = float(start), float(stop), float(step)
    size = int(round((stop-start)/step))
    result = [start] * size
    for i in xrange(size):
        result[i] += i * step
    return result

After copying and pasting the function definition in your Python interpreter, do:

>>> lx = lrange(0.0, 5.0, 0.5)
>>> print lx
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5]

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:

>>> ly = map(lorentzian, lx)

NumPy arrays are even more elegant, and they allow:

>>> ay = lorentzian(ax)

Almost magic, isn't 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 ?
  File "<stdin>", line 2, in lorentzian
TypeError: unsupported operand type(s) for -
>>>

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):

>>> import profile
>>> xl = lrange(0, 10, 0.0001)
>>> xa = arange(0, 10, 0.0001)
>>> profile.run('yl = map(lorentzian, xl)')
         100002 function calls in 2.200 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   100000    1.000    0.000    1.000    0.000 <stdin>:1(lorentzian)
        1    1.200    1.200    2.200    2.200 <string>:1(?)
        0    0.000             0.000          profile:0(profiler)
        1    0.000    0.000    2.200    2.200 profile:0(yl = map(lorentzian, xl))

>>> profile.run('ya = lorentzian(xa)')
         3 function calls in 0.090 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.090    0.090    0.090    0.090 <stdin>:1(lorentzian)
        1    0.000    0.000    0.090    0.090 <string>:1(?)
        0    0.000             0.000          profile:0(profiler)
        1    0.000    0.000    0.090    0.090 profile:0(ya = lorentzian(xa))

>>>

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.

Prev | Home | Next
Reading the Qt Documentation | Up | PyQwt


First Steps with Sip

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

First Steps with Sip
GUI Programming with Python: QT Edition
Prev Next

Appendix C. First Steps with Sip

Table of Contents
Introduction
How sip works
Creating .sip files
Things sip can't do automatically
Where to look to start writing your own wrappers/bindings
Sip usage and syntax
Directives
Accepted C++ / Qt constructs
SIPLIB Functions
Jim Bublitz

Jim Bublitz knows far more about sip, the tool used to wrap C++ libraries for Python, than I do. For instance, he's the author of the bindings to the KDE2 libraries. For these reasons, I asked him to write an appendix on using sip.

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 isn't 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 isn't a translation of Qt into Python—instead, 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 it's 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:

  • Re-implementation of C++ virtual methods in Python.

  • Sub-classing C++ classes from Python.

  • Access to a C++ class's protected methods.

  • Overloading of C++ functions and methods with different parameter type signatures.

  • Automatic translation between C++ classes and similar (but more appropriate) Python types.

Prev | Home | Next
PyQwt | Up | How sip works


Bibliography
Prev Next

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. It's the closest the programming field has to a canon.

A lot of the generally recommended books, such as Fowler's 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.

Frederick Brooks, 1995, The Mythical Man-Month: Essays on Software Engineering, Addison-Wesley Publishing Company.

Gaius Iulius Caesar, 1954, De Bello Gallico, Bewerkt door Dr. J.J.E. Hondius, Zesde Uitgave.

George van Driem, 1987, A Grammar of Limbu, Mouton de Gruyter.

Bruce Eckel, 2000, Thinking in Java: second edition.

Craig A. Finseth, 1999, The Craft of Text Editing: or Emacs for the Modern World, http://www.finseth.com/~fin/craft.

John E. Grayson, 2000, 2000, Python and Tkinter Programming, Manning.

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.

Diana Wynne Jones, 1988, Howl's Moving Castle, Methuen.

Daniel Jurafsky and James H. Martin, Speech and Language Processing: An Introduction to Natural Language Processing, Computational Lingusitics and Speech Recognition, 2000, Prentice Hall.

Mark Lutz, 1996, Programming Python, O'Reilly & Associates.

—, 2000, Learning Python, O'Reilly & Associates.

Gareth McCaughan, Rhodri James, and Paul Wright, 2001, Livewires Python Course, Scripture Union, http://www.livewires.org.uk/python/.

Steve McConnell, 1993, Code Complete, Microsoft Press.

Steve Oualline, 1995, Practical C++ Programming, O'Reilly & Associates.

Boudewijn Rempt, "Python's 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.

Al Stevens, "YAPP": Yet Another Programming Paradigm, 329, October 2001, 2001, 105-111, Dr Dobbs Journal, CMP, Edited by Jonathan Erickson.

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.

Prev | Home | Next
SIPLIB Functions |  | Index


Introduction

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Introduction
GUI Programming with Python: QT Edition
Prev Next

Chapter 1. Introduction

Table of Contents
Python
GUI programming with Python
About the BlackAdder IDE

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. More's the pity that there aren't 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.

It's amazing how many software development environments have been designed with something else than developer productivity as the main goal. There's 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 doesn't 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.

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 it's ‘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 don't need pre-processors or compilers, you can get by with nothing more than a text editor, like XEmacs Nedit, or MultiEdit. Indeed, I've 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 Apple's OS X, too).

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:

  • Compiled to byte-code, interpreted by a virtual machine.

  • High-level data structures: lists, tuples and dictionaries

  • Dynamic: you can even add new base-classes to an existing object, run-time.

  • Portable: the same Python bytecode will run — depending on which version of Python you use and which C or C++ extensions are used — on Unix, Windows, MacOS, Amiga, Palm OS and many others.

  • Extensible with modules written in C or C++: there is no performance penalty for calling native code, as there is when calling native code from Java.

  • An object-oriented programming model, but also supports functional programming (a bit) and old-fashioned structured programming.

  • Enormous set of extension libraries: for database access, high-performance number-crunching, for sound-file analysis, for GUI programming and countless other tasks.

  • Built-in regular expression engine that works on both regular and Unicode strings.

  • Use of indentation instead of braces begin/end pairs to delimit blocks of code. This practically forces readable code.

Your Python code resides in files, ending with .py suffix. These files can be grouped in modules, in the form of directories with an indexfile called __init__.py, and you can import elements from modules and files in other files. There is one file you use to start your application. It will usually simply import the necessary modules and start the application explicitly in a main (args) function.

Maybe the introduction is bit early to start with actual code examples, but let's have an example of a Python bootstrap script anyway:

Example 1-1. Bootstrapping a Python application

#!/usr/bin/env python                                        (1)
#
# bootstrap.py
#

import sys                                                   (2)
from myapp import SomeClass                                  (3)

def main(args):                                              (4)
    class=SomeClass(args)
    class.exec_loop()

if __name__=="__main__":                                     (5)
    main(sys.argv)
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 won't 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 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.

Prev | Home | Next
Acknowledgments |  | GUI programming with Python


Installation

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Installation
GUI Programming with Python: QT Edition
Prev Next

Chapter 2. Installation

Table of Contents
Installing BlackAdder
Installing sip and PyQt without BlackAdder

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 buying—so don't forget to read the README files!

Installing BlackAdder

PyQt can be used on Windows, Unix/X11 and soon OS X, but BlackAdder is only available for Windows and Unix/X11 (due to licensing restrictions).

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 it's simply a matter of installing the components, one after another. Every component is provided in a comfortable Windows installer package.

Installing BlackAdder

BlackAdder will now be ready to run — a friendly icon has appeared on your desktop, just begging to be clicked.

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

boud@calcifer:~/tmp > rpm --install BA-personal-1.0Beta3-1_tkc_suse71.i386.rpm

on the command line, if you're installing the 3.1 beta for SuSE Linux. The actual name of the rpm will vary, of course.

KPackage with the BlackAdder rpm

Additionally, you might want to set two environment variables in your .bashrc file. Installing BlackAdder and Python in the default location isn't necessary, but if you deviate from the standard BlackAdder installation directory, you need to set the following variables.

export BLACKADDERDIR=/usr/lib/BlackAdder export
BLACKADDERPYTHON=/usr/local/bin/python

Now, simply typing "ba" on the command line will start BlackAdder.

Prev | Home | Next
Introduction to the BlackAdder IDE | Up | Installing sip and PyQt without BlackAdder


Interface

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Interface
GUI Programming with Python: QT Edition
Prev Next

Chapter 3. Interface

Table of Contents
Menubar
Toolbars
Project management
BlackAdder Configuration
Editing
Python shell
Conclusion

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. You'll 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.

BlackAdder and its ‘About' dialog.

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 interpreter executable (either python or python.exe on Windows), as described in the Section called Linux in Chapter 2.

Python has given the ghost.

However, if all is well, you can start exploring your new development environment.

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.

Prev | Home | Next
Installing sip and PyQt without BlackAdder | Up | Toolbars


Introduction to Python

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Introduction to Python
GUI Programming with Python: QT Edition
Prev Next

Chapter 4. Introduction to Python

Table of Contents
Programming fundamentals
The Rules
Constructions
Conclusion

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 doesn't understand plain English, I'm 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 can't really be done, I'm 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 haven't 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.

Programming fundamentals

Please don't think that I can teach you programming in just the space of this section — you need to read some good books for that, such as Steve McConnel's Code Complete. What I can do is show you what the fuss is all about.

Computers do not do anything of their own volition: ultimately, someone has 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 you've 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.

Let's 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 can't 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 don't do this! On the other hand, asking Python about the captain's age or the contents of a bathtub that's 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.

Let's 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 don't type the three > signs or the three dots — Python does this for you.

Python 2.1.1 (#1, Aug 11 2001, 20:14:53) [GCC 2.95.2 19991024
(release)] on linux2 Type "copyright", "credits" or "license"
for more information.
>>> class FirstClass:
...     def __init__(self, value):
...             self.item=value
...     def printValue(self):
...             print self.item
...
>>> firstObject=FirstClass(value="BlackAdder goes forth")
>>> firstObject.printValue
<method FirstClass.printValue of FirstClass instance at 0x80db1f4>
>>> firstObject.printValue()
BlackAdder goes forth
>>>

If you type neatly and without mistakes, the contents of the Python interpreter window might look like this. Let's look at what happens: we have defined a ‘class' — that's 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 — it's 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 doesn't 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 number) ‘BlackAdder goes forth', you can later call the printValue() function, which will be able to do something with that value.

In order to call—that is, ask Python to execute— a function, you must add brackets after the function name; the parameters always go between the brackets. You don't have to put self between brackets, for Python does this for you. If you don't 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 instance 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 BlackAdder's 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 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, don't 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 I've ever read.

Prev | Home | Next
Conclusion | Up | The Rules


Debugging

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Debugging
GUI Programming with Python: QT Edition
Prev Next

Chapter 5. Debugging

Table of Contents
Running scripts
Setting breakpoints
Stepping along
Debugging Techniques
If all else fails

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, really—in most cases it's 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 we'll use the debugger included in BlackAdder to find bugs in some simple scripts. If you don't understand the actual code yet, don't 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 you've ever tried to actually trace an application using one of these, you'll 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:

Python 2.1.1 (#1, Aug 11 2001, 20:14:53)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> import pdb
>>> import button
>>> pdb.run("button.main([])")
> /home/boudewijn/doc/pyqt/ch23/<string>(0)?()
(Pdb) continue
> /home/boudewijn/doc/pyqt/ch23/<string>(1)?()
(Pdb) continue
Traceback (most recent call last):
  File "button.py", line 26, in slotSlot
    i = 1/0
ZeroDivisionError: integer division or modulo by zero
--Return--
> /home/boudewijn/doc/pyqt/ch23/<string>(1)?()->None
(Pdb) w
> /home/boudewijn/doc/pyqt/ch23/<string>(1)?()->None
(Pdb) l
[EOF]
(Pdb) q
>>>

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 don't know where to stop debugging. If you are debugging a piece of code that contains the statement string.join(), you probably don't want to single-step into the string.py module, which is part of the Python system library—and 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.

The BlackAdder debugger.

The PyQt library includes another, simpler debugger, called eric. This application is no longer maintained, so I won't spend much time here describing the way it works. It does, however, provide a very nice example of using regular expressions and PyQt's QCanvas widget. You can start eric by typing eric on the command-line.

Eric, the debugger included in PyQt.

Running scripts

The BlackAdder toolbar has two buttons for debugging your code. You can either debug a single script, or the whole project.

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.

Prev | Home | Next
Conclusion | Up | Setting breakpoints


Qt Concepts

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Qt Concepts
GUI Programming with Python: QT Edition
Prev Next

Chapter 6. Qt Concepts

Table of Contents
Python, Qt and PyQt
As simple as they come
A better Hello World
Designing forms
Conclusion

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.

Python, Qt and PyQt

Unlike a tool like Visual Basic, which consists of a GUI engine with a scripting language built-in, Python does not have a native GUI interface. But there are many GUI libraries available for Python — examples are wxPython, Tkinter, PyGTK, PyFLTK, FoxPy, and PyQt. PyQt is based on Qt, an advanced GUI library for Windows and Unix written in C++ by Eirik Eng and Arnt Gulbrantsen of Trolltech in Norway. It's quite easy to wrap C++ or C libraries so they can be used from Python — and when Phil Thompson was looking around for a good GUI library for Python he decided to wrap Qt, producing PyQt. PyQt forms the basis for the BlackAdder rapid development environment.

Qt is very advanced: the library offers a large set of well-designed screen objects, or widgets, and many utility classes. In addition, Qt has a clean object-oriented design that is easy to grasp and intuitive to use. PyQt applications can run without any change, without recompiling, both on Windows and Unix/X11 systems — and soon on Apple's OS X, too.

PyQt widgets can be drawn in styles, to make them appear exactly like the native widgets of the operating system the application runs on (or like something different altogether, if you want).

There are two kinds of objects in the Qt library— visual and non-visual. The mother of all visual objects is QWidget, widget being the term Qt uses for what the Windows world usually calls control. There are simple widgets such as labels and buttons, and complex widgets such as canvas views. Even dialog windows are descended from QWidget.

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 you'll have a working application where you would still be hacking the first prototype in C++.

A note on versions: PyQt consists of at least three components, Python, Qt and PyQt itself. Additionally, there is PyKDE, the bindings to the KDE Desktop Environment for Unix. That's four components with almost unrelated version numbering. Qt has been through three versions to date, just like PyQt. However, PyQt 3.x can be compiled against Qt 1.x, Qt 2.x and Qt 3.x.

The differences between versions of PyQt consist of the range of versions of Qt supported, and certain matters of organization and some implementation details.

The relation between PyQt and Python is even more nebulous: PyQt works best with the latest version of Python, but earlier versions are known to work, too.

Anyway, just keep in mind that PyQt 3.x can use Qt 2.x or Qt 3.x. This book was mostly written using PyQt 2.x for Qt 2.x, but I've tried to include as much information about PyQt 3.x and Qt 3.x as I could.

Prev | Home | Next
PyQt fundamentals | Up | As simple as they come


Signals and Slots in Depth

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Signals and Slots in Depth
GUI Programming with Python: QT Edition
Prev Next

Chapter 7. Signals and Slots in Depth

Table of Contents
The concept of signals and slots
Connecting with signals and slots
Disconnecting
A parser-formatter using signals and slots
Conclusion

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.

The concept of signals and slots

The problem in a nutshell: imagine you have an application, and the application shows a button on screen. Whenever the button is pressed, you want a function in your application to execute. Of course, you'd prefer that the button doesn't know much about the application, or you would have to write a different button for each application. In the next example, the button has been coded to work only with an application that has the doSomeApplicationSpecificFunction function.

Example 7-1. A stupid button which is not reusable

#
# stupid_button.py — this button is not reusable
#
class Button:

    def __init__(self, application):
      self.application = application

    def clicked(self):
        self.application.doSomeApplicationSpecificFunction()

class Application:

    def __init__(self):
        self.button=Button(self)

    def doSomeApplicationSpecificFunction(self):
        print "Function called"

app=Application()
app.button.clicked() # simulate a user button press

Callbacks

This is no solution— the button code isn't 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.

Example 7-2. A simple callback system

#
# callback.py — handing the function over the the app
#
class Button:

    def __init__(self, function):
        self.callbackFunction = function

    def clicked(self):
        apply(self.callbackFunction)

class Application:

    def __init__(self):
        self.button=Button(self.doSomeApplicationSpecificFunction)

    def doSomeApplicationSpecificFunction(self):
        print "Function called"

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(). You'll see that idiom quite often when we subclass Qt classes:

class MyWidget(QWidget):

    def __init__(self, *args):
        apply(QWidget.__init__, (self,) + args)

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):

    def __init__(self, parent):
        QWidget.__init__(self, parent)

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 the Section called Methods and functions in Chapter 4 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 don't 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:

>>> class O(QObject):
...     def __init__(self, *args):
...             QObject.__init__(self, *args)
...
>>> 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.

Action registry

Unfortunately, this callback system is not quite generic enough. For example, what if you wanted to activate two functions when the button is pressed? While this is not likely in the simple example, under more complex situations it often occurs. Think of a text editor where editing the text should change the internal representation of the document, the word count in the statusbar, and the edited-indicator in the titlebar. You wouldn't want to put all this functionality in one function, but it is a natural fit for signals and slots. You could have one signal, textChanged, that is connected to three functions: changeText(), setWordCount(), setEdited().

Wouldn't it be extremely comfortable to simply have a central registry where interested parties could come together? Something like:

Example 7-3. A central registry of connected widgets

#
# registry.py — a central registry of connected widgets
#
class Registry:

    def __init__(self):
        self.connections={}                                  (1)

    def add(self, occasion, function):                       (2)
        if self.connections.has_key(occasion) == 0:
            self.connections[occasion]=[function]
        else:
            self.connections[occasion].append(function)

    def remove(self, occasion, function):                    (3)
        if self.connections.has_key(occasion):
            self.connections[occasion].remove(function)

    def execute(self, occasion):
        if self.connections.has_key(occasion):               (4)
            for function in self.connections[occasion]:
                apply(function)

registry=Registry()
                                                             (5)
class Button:
                                                             (6)
    def clicked(self):
        registry.execute("clicked")

class Application:

    def __init__(self):
        self.button=Button()
        registry.add("clicked", self.doAppSpecificFunction)
        registry.add("clicked", self.doSecondFunction)       (7)

    def doAppSpecificFunction(self):
        print "Function called"

    def doSecondFunction(self):
        print "A second function is called."

app=Application()
app.button.clicked()
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 wouldn't 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.

Signals and slots

We've just outlined the problem which the developers of Qt at Trolltech have solved in a unique and flexible manner. They created the concept of signals and slots. signals are sent by an object that wants to tell the world something interesting has happened, and by connecting the "signals to the slots", those signals arrive at the slots of the objects that are interested.

On the whole the concept is really neat and clean and the implementation well-executed. What's more, the concept is even better suited to Python than it is to C++. If you want to use signals and slots in C++, you have to work with a preprocessor, called moc, and indicate with special macros which function can be called as a slot, and which function is a signal. All that is completely unnecessary in Python, where a signal is a string, and any function can be a slot.

Signals and slots are not magic, of course. As with our simple Python registry, there has to be a registry of objects that are interested in signals. This registry is updated by the connect and disconnect functions; both are member functions of the QObject class. The registry, as far as Python signals and slots is concerned, is kept by the sip library. Signals and slots that are defined in the underlying C++ library are maintained by the QObject class itself.

In a nutshell, signals and slots are the solution Qt provides for situations in which you want two objects to interact, while keeping that fact hidden from them.

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 don't connect two class instances, but rather a physical object, such as a keyboard, with an 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.

Prev | Home | Next
Conclusion | Up | Connecting with signals and slots


Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from books


Python Objects and Qt Objects

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Python Objects and Qt Objects
GUI Programming with Python: QT Edition
Prev Next

Chapter 9. Python Objects and Qt Objects

Table of Contents
Pointers and references
Circular references
Qt objects, Python objects and shadow objects
References and ownership
Other C++ objects
Connecting signals and slots
Object and class introspection

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 you're leaking memory like a sieve, then this is the place to turn to.

Pointers and references

In order to be able to determine the relations between Python objects and C++ objects it is necessary to first gain a good understanding of what an object is, exactly, and what constitutes a reference to an object.

In C++, an object is simply a chunk of memory that contains executable bytes and data bytes. The executable bytes represent the functions, and the data bytes represent the values of the object variables. Of course, this is a simplified representation: the functions are shared by all objects of the same class, and there is some serious (and platform dependent) pointer logic needed to find them. But, basically, a C++ object is simply a stretch of memory that has to be allocated explicitly by the developer (using new()), and also deallocated explicitly by the developer, with delete().

The object can be accessed by other parts of the application as long as its location in memory is known: the variable that contains the location is a pointer. If a programmer knows the size of an object, he can do fancy things (such as loop through the memory by adding the size of the object to the pointer) to get at the location of the next object.

However, once the pointer variable is lost, there's no longer a certain way of getting at the location of the object, and there's no way to delete the object—the memory will remain occupied for as long as the application runs, and there's no way it can be useful! This is called a memory leak, and is undoubtedly a bad thing.

One of the strengths of Python is that the programmer is freed of the responsibility 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:

Example 9-1. refs.py - showing object references

#
# 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:

<__main__.theClass instance at 0x81d9cb4>
[<__main__.theClass instance at 0x81d9cb4>]
{'key': <__main__.theClass instance at 0x81d9cb4>}

The object instance (0x81dcb4 is the object's 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.

Prev | Home | Next
Unicode strings | Up | Circular references


Qt Class Hierarchy

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Qt Class Hierarchy
GUI Programming with Python: QT Edition
Prev Next

Chapter 10. Qt Class Hierarchy

Table of Contents
Hierarchy
Base classes
Application classes
Widget foundations: QWidget
Basic widgets
Advanced widgets
Layout managers
Dialogs and Standard Dialogs
Qt Utility classes and their Python equivalents

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.

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 the Section called Object and class introspection in Chapter 9.

Figure 10-1. Qt Inheritance Hierarchy (only the most important classes)

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.

In addition to the inheritance hierarchy, there is an ownership hierarchy, where a window will own, for instance, toolbars, a menubar and a statusbar. If the window is deleted, all child objects will be deleted, too, and if a keypress event arrives for an application, it will traverse the tree until it arrives at the right spot. The ownership hierarchy comes into existence by creating objects with their owner object as parent - see the Section called References and ownership in Chapter 9 about this principle.

Figure 10-2. Object Ownership Hierarchy

Prev | Home | Next
Object and class introspection | Up | Base classes


Qt Designer, BlackAdder and uic

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Qt Designer, BlackAdder and uic
GUI Programming with Python: QT Edition
Prev Next

Chapter 11. Qt Designer, BlackAdder and uic

Table of Contents
Introduction
Advanced Designer topics

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 BlackAdder's 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 menu's, 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++

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 application.

Starting out with the designer module

Beginning this process is easy, at least as far as GUI design is concerned! After choosing New from the File menu, you will be presented with a dialog that asks you to choose what kind of item you want to create:

Selecting a template for a new GUI design.

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 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.

The Dialog template, which appears rather bare.

Wizard is more interesting. This template, based on QWizard, offers everything you need to create these popular "hand-holding" forms.

The wizard template.

Configuration Dialog is interesting, too. It is meant for application-wide preference dialogs, with a listbox containing configuration categories on the left, and a new set of tabbed forms for each configuration category on the right. Note that you can just as easily put pixmaps in a listbox as text strings. It is far more professional to give the user icons to select from instead of text labels in a listbox.

The Configuration dialog template.

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 dialog-with-the-buttons-on-the-right template.

The last default template is for creating a bottom-buttoned dialog with a tab strip on top.

The tabbed dialog template.

Creating a design

I'm assuming that you are familiar with the concept of drawing a gui on a grid. Click on the icon that represents the widget you want, click on the grid, drag it to where you want, and alter any properties you want. It's as simple as that—I can't make it any more difficult for you.

A partially filled-in form.

Grouping widgets

One thing to keep in mind is the essential difference between container widgets and normal widgets. Container widgets can hold other widgets in a parent-child relation. 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.

A groupbox holding radio buttons.

Layout management

A layoutmanager is a container, too, but here the procedure is just the other way around. You first create the widgets. Then, you select all widgets that should be managed, and then select one of the layout managers (horizontal, vertical or grid). Every time you add a new widget to the container (or a spacer object) you will break the layout and have to recreate it. You can also nest layout managers, to create more complicated effects.

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.

Tab order and accelerators

A good GUI allows the user to do everything with just the keyboard. For this, it is necessary to give every control its own accelerator key. There are two possibilities for creating these accelerators. Either the widget has a label component of its own, in which case typing an & before the letter you want to make the accelerator will suffice. Or, and this is more usual, the widget is purely graphical, but can be associated with a QLabel. Again, the & defines the accelerator, but you must still somehow link the label with the widgets. This is done through the buddy option in the properties sheet. If you select the label, and then enter the name of the associated widget in the buddy field, a link will be made.

Selecting a buddy.

BlackAdder can check for duplicate accelerators. In the Edit menu, select the option Check Accelerators. There's 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!

Setting the tab order.

Setting the tab order right now becomes one of those pleasurable little tasks that give a developer a bit of thinking time.

Prev | Home | Next
Qt Utility classes and their Python equivalents | Up | Advanced Designer topics


Application Frameworks

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Application Frameworks
GUI Programming with Python: QT Edition
Prev Next

Chapter 12. Application Frameworks

Table of Contents
Architecture: models, documents and views
Macro languages
Project layout

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 don't 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, I'll show you one useful way of laying out the project structure. In the next few chapters we'll build this framework into a real application.

Architecture: models, documents and views

Let's start with the basics. Applications are, essentially, interfaces that manipulate data. Whether you are handling records from a database, an object tree from a parsed HTML page, a game world, or a stream from a network socket, there is always a reality that is mediated to the user with an interface.

From this, it follows naturally that it would be a good idea to separate bits of software that handle the data from the interface. After all, you might want to manipulate the same data with a different interface. In some development environments this is quite difficult to achieve. Visual Basic, for instance, almost mandates that you entangle your data-mangling code with your GUI code. On the other side of the scale, SmallTalk has explicit support for the most extended form of the celebrated Observer pattern — with the Model/View/Controller framework for the SmallTalk user interface (or, in later versions, the Model-View-Presenter architecture).

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, doesn't 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 doesn't 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. That's why Java's 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, 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.

A document-view framework

The most basic architecture in which application model and interface are separated is the document-view paradigm. Here, you have two basic modules: one representing your data (the document), and one representing the interface that shows the data to the user (the view). This architecture is prevalent in the Windows world: the entire Microsoft MFC library is based on its principles, and it is also popular in the Unix world, where many KDE applications are based on it.

The document-view architecture

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, we'll work with the framework to build a real application. We'll 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.

Example 12-1. A simple document-view framework

class DocviewDoc(QObject):

    def __init__(self, *args):
        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. That's 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 the Section called QColor in Chapter 10, 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):
    def __init__(self, doc, *args):
        apply(QWidget.__init__, (self, ) + args)
        self.doc = doc
        self.connect(self.doc, PYSIGNAL("sigDocModified"),
                     self.slotDocModified)
        self.slotDocModified(self.doc.isModified())

    def slotDocModified(self, value):
        if value:
            self.setBackgroundColor(QColor("red"))
        else:
            self.setBackgroundColor(QColor("green"))

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()

The controller keeps a dictionary of actions, making it easier to refer to those actions when populating the menu and toolbars. The dictionary can also be used to export functionality for a macro language, by calling the QAction.activated() slot, which is connected to the relevant slots in the controller. The pixmap is in the form of an inline XPM image, which is not shown here.

    def initActions(self):
        fileQuitIcon=QIconSet(QPixmap(filequit))
        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 menu's 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)

    def initStatusBar(self):
        self.statusBar().message("Ready...")

Here the document, or application model, is initialized.

    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 we're 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 really 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.

    #
    # 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 doesn't 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:

A very simple document-view framework application

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 mouseDoubleClickEvent(self, ev):
        self.doc.slotModify()

This bypasses the controlling application (DocviewApp) and leads to an uncomfortably tight coupling between view and document. Another way to notify the document of the double-click is to let the view emit a signal, which can be caught by the application object and connected to the document slot. Replace the previous function with the following function in the DocviewView class instead:

    def mouseDoubleClickEvent(self, ev):
        self.emit(PYSIGNAL("sigViewDoubleClick"),())

And to the DocviewApp:

    def initView(self):
        self.view = DocviewView( self.doc, self)
        self.setCentralWidget(self.view)
        self.connect(self.view, PYSIGNAL("sigViewDoubleClick"),
                     self.slotEditDoc)

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.

Prev | Home | Next
Creating real applications with PyQt | Up | Macro languages


Actions: menus, toolbars and accelerators

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Actions: menus, toolbars and accelerators
GUI Programming with Python: QT Edition
Prev Next

Chapter 13. Actions: menus, toolbars and accelerators

Table of Contents
Actions
Menus
Toolbars
Keyboard accelerators
Setting an application icon

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. Let's take a deeper look at the possibilities of actions, toolbars and menus.

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 action—such as redo, when there's nothing to redo—and you're 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:

  • Pull-down menus text

  • Tooltip text

  • What's This text

  • 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:

Example 13-1. Defining a complex toggle action

    planAction=QAction(self)                                 (1)
    planAction.setIconSet(QIconSet(QPixmap(plan)))           (2)
    planAction.setText("Plan")                               (3)
    planAction.setMenuText("&Plan ...")                      (4)
    planAction.setOn(0)
    planAction.setStatusTip("Enable the cunning plan")       (6)
    planAction.setToolTip("Enables the cunning plan")        (7)
    planAction.setWhatsThis(                                 (8)
"""Plan

Selecting plan enables the cunning plan
for this window.""")
    planAction.setAccel(QAccel.stringToKey("CTRL+C"),)       (9)
    planAction.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 icon—or 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 hasn't 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 wouldn't want the shortcut to show up on toolbar captions—they don't work there, so you can set them in the menu text, by prefixing an ampersand (&) to the shortcut letter.
Sometimes, a certain action toggles an application state on or off. Examples include setting toolbar captions of toolbar icon sizes, or bold or normal fonts. By grouping actions in action groups, you can create mutually exclusive groups (such as centered, justified or ragged text in a text editor).
This text appears in the statusbar of the mainwindow when the user presses but doesn't release the menu option or toolbar button associated with the action.
This is a longer text that appears in the form of a yellow note when the user presses the optional ‘what's this' button, which appears on the toolbar.
The keyboard accelerator that activates this action—for instance, pressing the control and the s key together—will trigger the activated() signal, which might be connected to the save() slot function. It is easiest to construct this using QAccel's stringToKey() function. Not only is this very convenient, it can also be translated to languages where a different shortcut key is preferred, by using either pygettext or PyQt's tr(). See Chapter 25 for more information on internationalizing an application.
Of course, if you create a toggle action, it is nice to be able to set the initial state—which is what this does.

The QAction class can emit two signals:

  • 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.

        self.connect(planaction,
                     SIGNAL("activated()"),
                     self.slotExecuteCunningPlan)

or, for a toggle action:

        self.connect(planaction,
                     SIGNAL("toggled(bool)"),
                     self.slotActivateCunningPlan)

All that remains, is to add the actions to pulldown menus or toolbars:

        self.planaction.addTo(self.planMenu)
        self.planaction.addTo(self.toolBar)

Prev | Home | Next
Project layout | Up | Menus


Automatic testing with PyUnit

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Automatic testing with PyUnit
GUI Programming with Python: QT Edition
Prev Next

Chapter 14. Automatic testing with PyUnit

Table of Contents
About unittests
Starting out
A first testcase
Collecting tests in a test suite
A more complicated test
Large projects
Testing signals and slots
Conclusion

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.

About unittests

Have you ever done maintenance on a large application and broken something because you changed something, somewhere? Or worse, only noticed the breakage when a user mailed you? Have you ever begun writing an application, but were unable to complete it because the whole castle of cards collapsed due to excessive fragility?

It has probably happened to you, and it certainly has happened to me. Testing software is a boring chore, and besides, everything has to be finished yesterday, or today at the latest. However, not testing will cost you a lot of time, too, and it's more fun to program than to bug-hunt. It would be best if automated testing could be made part of the edit-compile-crash cycle.

This has occurred before to a lot of people, but the honor of ‘inventing' automatic unit-testing belongs to Erich Gamma and Kent Beck - familiar names to developers everywhere. They started writing unit-test frameworks for SmallTalk, moving on to Java and other languages.

The idea is simple but ingenuous: first the developer writes his test, then the class that will make the test work; the process is repeated until the created code fits the application you're developing. All the while, you will get instant feedback from a small GUI app that runs your tests and shows you a green progressbar when 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 isn't as much fun.

All is well - the bar is green!

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 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.

It's 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...

Prev | Home | Next
Setting an application icon | Up | Starting out


A More Complex Framework: Multiple Documents, Multiple Views

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

A More Complex Framework: Multiple Documents, Multiple Views
GUI Programming with Python: QT Edition
Prev Next

Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views

Table of Contents
Introduction
Document/View Manager
The Document Manager
Document
View
The actual application
Conclusion

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. It's a pity that in PyQt 2 dockable windows are not supported — for that you need to use QDockWindow in PyQt 3.

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.

The Gimp littering my desktop

Finally, there is the style made popular by IDE's 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' IDE's: 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.

KDevelop — the epitome of an interface with tabs.

I have chosen to disregard the more exotic styles of multiple-document interfaces. There are many varieties of these, such as vi's, in which you have a set of documents you can visit one after another (but not go back) and Emacs, which gives you a set of buffers that may or may not be presented in another window or another frame (where window and frame are the exact opposite of what you'd expect). These oddities should remain just that — oddities.

As an aside, it is interesting to note that all these styles originated with programming environments. It is often the case that a programming environment influences the developer in his interface decisions — and this might very well fail to support the needs of the user...

In this chapter, we first develop a document/view manager that can be used to manage a situation in which you have more than one document with more than one view. It will also handle keeping track of modifications to the documents, saving, 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.

Prev | Home | Next
Conclusion | Up | Document/View Manager


User Interface Paradigms

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

User Interface Paradigms
GUI Programming with Python: QT Edition
Prev Next

Chapter 16. User Interface Paradigms

Table of Contents
Tabbed documents
Back to the MDI windows
A row of split windows
A stack of documents
A more complex view management solution
Conclusion

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.

Tabbed documents

Like most user interface paradigms, the tabbed document paradigm has been popularized by current integrated development environments. A tabbed document collects all open documents in one window, with a row of tabs to facilitate easy navigation of documents. This paradigm has become so prevalent that even the old stalwart of user interface conservatism, XEmacs, supports it.

It turns out to be remarkably easy to implement a tabbed document interface. First, let's determine what we want to get out of this component. It is the first of several generic components that can take views — i.e. QWidget's— and show them in an application workspace. All view managers should have the same API. That allows the user to choose his favorite way of working without giving us lots of work — because, from the point of view of the application, all view managers are exactly the same.

We will provisionally call the component that manages tabbed views TabManager. The TabManager is meant to be almost a drop-in replacement for the QWorkspace we used in the Chapter 15. Therefore, it should support most of the same functionality: adding, removing and listing actual views. Other capabilities of QWorkspace don't make sense: you cannot tile or cascade tabbed windows. There must be some way to indicate to the wrapping application whether the view manager supports these capabilities.

PyQt offers a QTabWidget, which fits the basics of our needs perfectly. However, in contrast with the QWorkspace, where merely creating a widget with the workspace as parent widget was enough to let it be managed, QTabWidget wants us to explicitly add pages, and thus widgets, to its list of tabs. Finally, it also allows the addition and removal of pages. We can also request a reference to the active view, and ask to be notified of page changes.

QTabWidget is used in the QTabDialog dialog window class, and makes use of QWidgetStack and QTabBar. QWidgetStack keeps a stack of widgets of which only one is shown at a time. QTabBar, which keeps a row of tabs. Tabs can be square or triangular (the latter is seldom seen nowadays, for it is very ugly), and shown on the top or bottom of the window.

Applications that handle documents that consist of several (but not many) pages often show a row of triangular tabs at the bottom of the window. You cannot set the tabs to appear at the side of the window. That's a pity, since it is a position that is quite often preferred by users.

Let us take a look at the implementation of a tabbed document manager:

"""
tabmanager.py - tabbed document manager for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class TabManager(QTabWidget):

    def __init__(self, *args):
        apply(QTabWidget.__init__,(self, ) + args)
        self.views=[]
        self.setMargin(10)

The TabManager is derived from QTabWidget. A simple python list of views is kept, otherwise we would not be able to retrieve a list of all open views for the ‘windows' menu. The margin between tab and document should really be a user-settable property, but we won't develop a user preferences framework until chapter Chapter 18.

    def addView(self, view):
        if view not in self.views:
            self.views.append(view)
            self.addTab(view, view.caption())
            self.showPage(view)

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 won't manage the layout of the page. This means that when you create a new window and resize the application window, the contents won't resize with it. Simply drawing the tab widget's attention to the page will suffice, however.

With PyQt's QWorkspace it was enough to create a widget with the workspace as its parent—the 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:

    def _createView(self, document, viewClass):
        view = viewClass(self._viewManager,
                         document,
                         None,
                         QWidget.WDestructiveClose)
        if self._docToViewMap.has_key(document):
            index = len(self._docToViewMap[document]) + 1
        else:
            index = 1
        view.setCaption(document.title() + " %s" % index)

        self._viewManager.addView(view)

        view.installEventFilter(self._parent)

        if self._viewToDocMap == {}:
            view.showMaximized()
        else:
            view.show()

        return view

To return to the TabManager class:

    def removeView(self, view):
        if view in self.views:
            self.views.remove(view)
            self.removePage(view)

    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, it's a simple change:

    def _removeView(self, view, document):
        try:
            self._docToViewMap[document].remove(view)
            self._viewManager.removeView(view)
            del self._viewToDocMap[view]
        except ValueError, e:
            pass # apparently already deleted

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 cascade(self): pass

    def tile(self): pass

    def canCascade(self):
        return FALSE

    def canTile(self):
        return FALSE

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.

Prev | Home | Next
Conclusion | Up | Back to the MDI windows


Creating Application Functionality

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Creating Application Functionality
GUI Programming with Python: QT Edition
Prev Next

Chapter 17. Creating Application Functionality

Table of Contents
Introduction
The view
The document
Saving and loading documents
Undo, redo and other editing functions
Conclusion

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 isn't 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.

Giving the project a name

Before starting out, we should decide upon a name for this project. I rather like kalam — a word which means pen or pencil, and which is derived from the Latin calamus. I have snapshotted the version we have at the end of each chapter so you can follow the development of this project. I didn't print the complete code of every class in every chapter, as that would be too tedious for words!

Prev | Home | Next
Conclusion | Up | The view


Application Configuration

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Application Configuration
GUI Programming with Python: QT Edition
Prev Next

Chapter 18. Application Configuration

Table of Contents
Platform differences
The Python way of handling configuration settings
Implementing configurations settings for Kalam
Settings in Qt 3.0
Conclusion

Every user has a preferred way of doing things — and a good application should be accommodating enough that important choices—such 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.

Platform differences

On a Windows system, most applications store user preferences in a central database, called the Registry. This book is not the place to argue about the wisdom or folly of keeping all application and system settings in one database, which can only be accessed with specialized tools. Most modern windows applications no longer use the once prevalent .ini standard. Using .ini, an application could store settings in a file in either the installation directory, the windows directory, or in one of two configuration files: win.ini or system.ini. Windows has only recently become a multi-user system, so it is still difficult to determine where to store user specific settings. In Windows 2000, I suggest the C:\Documents and Settings\{Username}\Local Settings\Application Data directory. In the registry, there is the HKEY_USER branch.

The Unix standard is not so much a standard as a gentle guide. You can store system-wide application settings in the /etc directory, or one of its subdirectories, or in the /usr/share directory together with resources... Or in /usr/local/share — or in any number of other places.

User settings on a Unix system are generally stored in so-called dot files, or dot directories. These are files or directories that start with a dot (.) and are thus ‘invisible' when the user asks for the contents of a directory using ls (or browses the directory with a filemanager like konqueror). The dot-files or dot-directories are located in the home directory of the user. A Unix home directory can be compared to the user's directory under C:\Documents And Settings in Windows 2000. It is generally found under /home. You can retrieve the location of this directory with:

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.

Prev | Home | Next
Conclusion | Up | The Python way of handling configuration settings


Using Dialog Windows

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Using Dialog Windows
GUI Programming with Python: QT Edition
Prev Next

Chapter 19. Using Dialog Windows

Table of Contents
Modal: a preferences dialog
Non-modal: Search and replace
Conclusion

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.

Modal: a preferences dialog

We will start with a preferences dialog. Nowadays, the taste is for dialogs with a strip of icons to the left that somehow indicates what section should be shown. But we will start out with a simple tabbed dialog that PyQt supports out of the box, and for which we don't have to draw icons (that's always the difficult bit, creating the artwork).

Designing the dialog

So: time to fire up the designer module of BlackAdder or Qt Designer!

The settings dialog - editor tab

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 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.

The settings dialog - interface tab

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 doesn't need a preview.

To fill in the preview I've 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.

The settings dialog - document tab

As for the document, we need two settings: the first is the document encoding. 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-dressing—actually 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 17—ultimately, 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 development—and it often feels that way indeed.

Creating the settings dialog window

The first part of the drill is well known: compile the frmsettings.ui file to Python using pyuic.

pyuic -x frmsettings.ui > frmsettings.py

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

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
import os, sys
from qt import *
import kalamconfig

from frmsettings import FrmSettings

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()

The DlgSettings dialog is a subclass of FrmSettings, which we created with Designer. In the constructor we create four objects for housekeeping purposes, to store changed settings until the user chooses to apply them by pressing OK, or to cancel.

These objects represent the editor font, the editor text color, the editor background color and the background color of the MDI workspace. As you can see from the calls to kalamconfig, actually implementing this dialog necessitated quite a few changes to the kalamconfig module.

The full source of kalamconfig is not so interesting for this chapter, but it is available with the rest of the code. To summarize the development: all settings are now retrieved and set through a single pair of get/set functions. There are a lot more settings, too. If a setting requires special handling, then the relevant get/set function is retrieved from a dictionary (you can just as easily store references to functions or classes in a dictionary as in strings, since everything is considered an object) and executed with apply(). If a setting is changed, a signal is emitted from the QApplication instance, which can be reached with the global variable qApp. Note how the actual signal identifier is constructed dynamically:

#
# kalamconfig.py
# Get and set - set emits a signal via Config.notifier
#
customGetSetDictionary = {
    "style" : (getStyle, setStyle),
    "workspace" : (getWorkspace, setWorkspace),
    "textfont" : (getTextFont, setTextFont),
    "textforeground" : (getTextForegroundColor, setTextForegroundColor),
    "textbackground" : (getTextBackgroundColor, setTextBackgroundColor),
    "mdibackground" : (getMDIBackgroundColor, setMDIBackgroundColor),
}

def set(attribute, value):
    if customGetSetDictionary.has_key(attribute):
        apply(customGetSetDictionary[attribute][1], (value,))
    else:
        setattr(Config, attribute, value)
    qApp.emit(PYSIGNAL("sig" + str(attribute) + "Changed"),
                         (value,))

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)

        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:

  • Background - general background color.

  • Foreground - general foreground color.

  • Base - background color text entry widgets

  • Text - the foreground color used with Base.

  • Button - general button background color

  • ButtonText - for button texts

  • Light - lighter than Button color.

  • Midlight - between Button and Light.

  • Dark - darker than Button.

  • Mid - between Button and Dark.

  • Shadow - a very dark color.

  • Highlight - a color to indicate a selected or highlighted item.

  • HighlightedText - a text color that contrasts to Highlight.

All colors are normally calculated from the Background color. Setting the background color of the editor with the convenience function setBackgroundColor() won't 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 isn't 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 Qt's highly optimized drawing engine becomes confused. This will be done in the slot function that's 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)

The preview for the interface style is initialized in initWindowViewCombo. Note that QLabel is rather more simple in its needs than QMultiLineEdit as regards colors. Here, you can just use the convenience function setBackgroundColor (setEraseColor() in Qt 3) to show the preview color for the MDI workspace.

    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):
        self.cmbStyle.clear()
        styles = kalamconfig.stylesDictionary.keys()
        styles.sort()
        try:
            currentIndex = 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.

# kalamconfig.py - styles dictionary
stylesDictionary = {
    "Mac OS 8.5" : QPlatinumStyle,
    "Windows 98" : QWindowsStyle,
    "Motif" : QMotifStyle,
    "Motif+" : QMotifPlusStyle,
    "CDE" : QCDEStyle
    }

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 doesn't return a list. If we'd written:

        styles = kalamconfig.stylesDictionary.keys().sort()

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 setStyle(self, style):
        kalamconfig.set("style", str(style))
        qApp.setStyle(kalamconfig.get("style")())

Changing a style is instantaneous in Kalam, if only because it is fun to run through all the styles and see the application changing under your fingers. Therefore, we immediately update the style setting, and call qApp.setStyle() to propagate the changes to the application widgets.

    def initWindowViewCombo(self):
        self.cmbWindowView.clear()

        workspaces = kalamconfig.workspacesDictionary.keys()
        workspaces.sort()
        try:
            currentIndex = 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)

Setting up the workspace selection combobox is similar to setting up the styles combobox. The only interesting point is the connection to setWorkspacePreview. This function updates the small image that shows what each option means. These images were made from snapshots, and scaled down with Pixie, a KDE graphics application (which is now obsolete).

    def setWorkspacePreview(self, workspace):
        workspace = str(workspace) + ".png"
        # XXX - when making installable, fix this path
        pixmap = QPixmap(os.path.join("./pixmaps",
                                      workspace))
        self.pxViewSample.setPixmap(pixmap)

As you can see, application development is messy, and I don't 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:
            currentIndex = 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:

# kalamconfig.py - encodings dictionary

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",
    "Turkish (iso 8859-9)": "iso-8859-9",
    "Inuit (iso 8859-10)": "iso-8859-10",
    "Thai (iso 8859-11)": "iso-8859-11",
    "Baltic (iso 8859-13)": "iso-8859-13",
    "Gaeilic, Welsh (iso 8859-14)": "iso-8859-14",
    "iso 8859-15": "iso-8859-15",
    "Cyrillic (koi-8)": "koi8_r",
    "Korean (euc-kr)": "euc_kr"}

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)
            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 tuple—and if ok is true, then we update the font of the preview and also set the textFont variable to reflect the users choice.

Finally, there's 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()

Calling the settings dialog window

In order to be able to call the dialog window, we must first create a new QAction and add it to a likely menu. This is done in KalamApp:

# 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)
        ...

The settingsSettings is connected to a new slot in KalamApp:

    # Settings slots

    def slotSettingsSettings(self):
        dlg = DlgSettings(self,
                          "Settings",
                          TRUE,
                          Qt.WStyle_Dialog)

The dialog window is constructed as a function-local variable. That means that if the function reaches its end, the dlg object is deleted. A settings dialog is typically modal. Whether a dialog is created modal or non-modal is determined in the constructor. The first argument to DlgSettings.__init__() is the parent window, in this case KalamApp. The second argument is a name. The third argument determines whether the dialog is modal—TRUE means modal, FALSE means non-modal. FALSE is also the default. The last argument can be any combination of widget flags. For a dialog box, Qt.WStyle_Dialog seems rather appropriate. Note that in Qt 3, this flag is renamed to Qt.WType_Dialog There are a whole lot of flags (the following list is based on Qt 2 - there have been some changes):

  • WType_TopLevel - a toplevel window

  • WType_Modal - Makes the widget modal and inplies WStyle_Dialog.

  • WType_Popup - this widget is a popup top-level window, it is modal, but has a window system frame appropriate for popup menus.

  • WType_Desktop - This widget is the desktop - you can actually use PyQt to paint on you desktop.

  • WStyle_NormalBorder - The window has a normal border.

  • WStyle_DialogBorder - A thin dialog (if you windowmanager on X11 supports that).

  • WStyle_NoBorder - gives a borderless window. However, it is better to use WStyle_NoBorderEx instead, because this flag will make the window completely unusable on X11.

  • WStyle_NoBorderEx - gives a borderless window.

  • WStyle_Title - The window jas a title bar.

  • WStyle_SysMenu - adds a window system menu.

  • WStyle_Minimize - adds a minimize button. On Windows this must be combined with WStyle_SysMenu to work.

  • WStyle_Maximize - adds a maximize button. See WStyle_Minimize.

  • WStyle_MinMax - is equal to WStyle_Minimize|WStyle_Maximize. On Windows this must be combined with WStyle_SysMenu to work.

  • WStyle_ContextHelp - adds a context help button to dialogs.

  • WStyle_Tool - A tool window is a small window that contains tools (for instance, drawing tools, or the step buttons of a debugger). The tool window will always be kept on top of its parent, if there is one.

  • WStyle_StaysOnTop - the window should stay on top of all other windows.

  • WStyle_Dialog - indicates that the window is a dialog window. The window will not get its own taskbar entry and be kept on top of its parent by the window system. This is the flag QDialog uses, and it is not necessary for us to explicitly pass it to DlgSettings.

  • WDestructiveClose - makes Qt delete this object when the object has accepted closeEvent(). Don't use this for dialog windows, or your application will crash.

  • WPaintDesktop - gives this widget paint events for the desktop.

  • WPaintUnclipped - makes all painters operating on this widget unclipped. Children of this widget, or other widgets in front of it, do not clip the area the painter can paint on.

  • WPaintClever - indicates that Qt should not try to optimize repainting for the widget, but instead pass the window system repaint events directly on to the widget.

  • WResizeNoErase - indicates that resizing the widget should not erase it. This allows smart-repainting to avoid flicker.

  • WMouseNoMask - indicates that even if the widget has a mask, it wants mouse events for its entire rectangle.

  • WNorthWestGravity - indicates that the widget contents are north-west aligned and static. On resize, such a widget will receive paint events only for the newly visible part of itself.

  • WRepaintNoErase - indicates that the widget paints all its pixels. Updating, scrolling and focus changes should therefore not erase the widget. This allows smart-repainting to avoid flicker.

  • WGroupLeader - makes this widget or window a group leader. Modality of secondary windows only affects windows within the same group.

You can combine these flags with the or (or |) operator.

Showing a modal dialog is a matter of simply calling exec_loop():

        dlg.exec_loop()
        if dlg.result() == QDialog.Accepted:
            kalamconfig.set("textfont", dlg.textFont)
            kalamconfig.set("workspace", str(dlg.cmbWindowView.currentText()))
            kalamconfig.set("style", str(dlg.cmbStyle.currentText()))
            kalamconfig.set("textbackground", dlg.textBackgroundColor)
            kalamconfig.set("textforeground", dlg.textForegroundColor)
            kalamconfig.set("mdibackground", dlg.MDIBackgroundColor)
            kalamconfig.set("wrapmode", dlg.cmbLineWrapping.currentItem())
            kalamconfig.set("linewidth", int(str(dlg.spinLineWidth.text())))
            kalamconfig.set("encoding", str(dlg.cmbEncoding.currentText()))
            kalamconfig.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,
                     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):

    def __init__(self, parent, doc, *args):
        ...
        self.editor.setFont(kalamconfig.get("textfont"))
        self.setWordWrap(kalamconfig.get("wrapmode"))
        self.setBackgroundColor(kalamconfig.get("textbackground"))
        self.setTextColor(kalamconfig.get("textforeground"))
        ...
        self.connect(qApp,
                     PYSIGNAL("siglinewidthChanged"),
                     self.editor.setWrapColumnOrWidth)
        self.connect(qApp,
                     PYSIGNAL("sigwrapmodeChanged"),
                     self.setWordWrap)
        self.connect(qApp,
                     PYSIGNAL("sigtextfontChanged"),
                     self.editor.setFont)
        self.connect(qApp,
                     PYSIGNAL("sigtextforegroundChanged"),
                     self.setTextColor)
        self.connect(qApp,
                     PYSIGNAL("sigtextbackgroundChanged"),
                     self.setBackgroundColor)
        ...

    def setTextColor(self, qcolor):
        pl = self.editor.palette()
        pl.setColor(QColorGroup.Text, qcolor)
        self.editor.repaint(TRUE)

    def setBackgroundColor(self, qcolor):
        pl = self.editor.palette()
        pl.setColor(QColorGroup.Base, qcolor)
        self.editor.setBackgroundColor(qcolor)
        self.editor.repaint(TRUE)

    def setWordWrap(self, wrapmode):

        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 interface style " +
                                        "will only be activated when you " +
                                        "restart the application.")
        ...

Prev | Home | Next
Conclusion | Up | Non-modal: Search and replace


A Macro Language for Kalam

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

A Macro Language for Kalam
GUI Programming with Python: QT Edition
Prev Next

Chapter 20. A Macro Language for Kalam

Table of Contents
Executing Python code from Python
Integrating macros with a GUI
Creating a macro API from an application
Conclusion

One thing that separates a run-of-the-mill application from a real tool—one that users will take refuge in day after day—is 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! Isn't 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 it's well worth the effort.

Executing Python code from Python

There are three basic ways to execute Python code that is not directly (i.e. imported as a module) part of your application. Single statements can be executed with eval(); we already encountered eval() in Chapter 18. Strings that contain more than a single statement of Python code can be executed with exec(), while Python code that is saved in a file can be executed with execfile().

Both eval() and exec() can be fed either plain text strings or pre-compiled Python byte code. You can create blobs of byte code with the compile() function, but you don't have to—it is simply a little more efficient to use bytecode if you execute the same code more than once. The evalfile() function reads in a file and executes the contents, which must be plain text. You cannot feed execfile compiled Python byte code.

Please note that eval(), exec(), execfile() and compile() are the real 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 won't 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.

Let's compare these locals and globals from an interactive Python session:

Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> globals()
{'__doc__': None, '__name__': '__main__',
 '__builtins__': <module '__builtin__' (built-in)>}
>>> def f():
...     a=1
...     print "locals: ", locals()
...
>>> globals()
{'f': <function f at 0x8124a94>, '__doc__': None,
 '__name__': '__main__',
 '__builtins__': <module '__builtin__' (built-in)>}
>>> f()
locals:  {'a': 1}
>>>

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.

Playing with eval()

eval() functions as if it executes a single line of code in the Python interpreter: it returns a value that represents the result of the evaluated expression. If the statement you give to eval() raises an exception, the surrounding code gets that exception, too. Playing around with eval() will give you a feeling for what it can do for you.

Figure 20-1. Playing with eval()

boud@calcifer:~/doc/pyqt/ch15 > python
Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> eval
<built-in function eval>
>>> eval("1==1")
1
>>> import string
>>> eval("string.split('bla bla bla')")
['bla', 'bla', 'bla']
>>> eval("string.split('bla bla bla')", {}, {})
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 0, in ?
NameError: There is no variable named 'string'
>>> eval("""from qt import *
... s=QString("bla bla bla")
... print str(s).split()
... """)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 1
    from qt import *
       ^
SyntaxError: invalid syntax
>>>

First, we take a look at what "eval" is for a beast. A built-in function. OK, let's 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 isn't known anymore. Trying to evaluate something more complicated, something that is not a single expression that returns a value (even if it's only None) doesn't work at all - which is why exec() exists.

Playing with exec

First, exec is really a statement, not a function, so it doesn't return anything. Just as with eval(), exceptions are propagated outside the code block you execute. You can feed exec a string, a compiled code object or an open file. The file will be parsed until an EOF (end-of-file) occurs, and executed. The same rules hold for the global and local namespace dictionaries as with eval() - but keep in mind that running exec might add new items to those namespaces.

Figure 20-2. Playing with exec

boud@calcifer:~/doc/pyqt/ch15 > python
Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> globals()
{'__doc__': None, '__name__': '__main__',
 '__builtins__': <module '__builtin__' (built-in)>}
>>> code = """
... import qt
... s = qt.QString("bla bla bla")
... print string.split(str(s))
... """
>>> exec code
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 4, in ?
NameError: There is no variable named 'string'
>>> import string
>>> exec code
['bla', 'bla', 'bla']
>>> globals()
{'__doc__': None, 'string': <module 'string' from
'/usr/lib/python2.0/string.pyc'>, '__name__':
 '__main__', '__builtins__': <module '__builtin__'
 (built-in)>, 'qt': <module 'qt' from
'/usr/lib/python2.0/site-packages/qt.py'>,
 'code': '\012import qt\012s = qt.QString("bla bla bla")\012print
string.split(str(s))\012', 's': <qt.QString instance at 0x8278af4>}
>>>

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 doesn't work: it throws a NameError because string isn't 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.

Playing with execfile()

The execfile() statement is rarely used; after all, it can't do anything beyond what the plain exec statement already does. execfile() functions exactly the same as exec, except that the first argument must be a filename (it doesn't need to be an open file object). Note that execfile() differs from import in that it doesn't create a new module in the global namespace. Note the difference between execfile() and import in the following output:

Figure 20-3. Playing with execfile()

Python 2.0 (#1, Mar  1 2001, 02:42:21)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> execfile("main.py")
Initializing configuration
{'kalam': <kalamapp.KalamApp instance at 0x825f014>,
 'app': <qt.QApplication instance at 0x814a2a4>, 'args': ['']}
Saving configuration
>>> import main
>>> globals()
{..., 'main': <module 'main' from 'main.py'>, ... }

In the middle of all the qt classes the main module of Kalam imports into globals, we find the main module itself, which isn't there if we just execfile main.py.

Prev | Home | Next
Conclusion | Up | Integrating macros with a GUI


Drawing on Painters and Canvases

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Drawing on Painters and Canvases
GUI Programming with Python: QT Edition
Prev Next

Chapter 21. Drawing on Painters and Canvases

Table of Contents
Working with painters and paint devices
QCanvas
Conclusion

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 cope—even on the most modern machines. In such cases, it is quite easy to replace the Python class with a Qt-based C++ object (you won't have to translate your whole Python application). See Appendix C for information on the wrapping of new C++ objects with sip.

Working with painters and paint devices

In Chapter 10 we introduced QPainter, for creating our little scribbling example application, event1.py. In this section we will take a closer look at the organization and use of the PyQt painting mechanism.

Painting in PyQt involves the cooperation of two classes: a QPainter and a QPaintDevice. The first is a very efficient abstraction of various drawing 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... We'll discuss the basics of printing in Chapter 24.

A painting example

There is little we can do using QPainter and QPaintDevices in our Kalam project — but after long and hard thinking I thought a rolling chart that counts how many characters the user types per minute might be a nice, although completely useless (and possibly frustrating) except for real productivity freaks.

Example 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...

"""
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 *

FIVE_SECONDS = 1000 * 5 #  5 seconds in milli-seconds
AVERAGE_TYPESPEED = 125 # kind of calibration
BARWIDTH = 3

TRUE=1
FALSE=0

No surprises here—just some declarations. I like to work with names instead of magic numbers, and to conform to practice in other programming languages, those names are in all-caps, even though they are not constants.

class TypoGraph(QPixmap):
    """ TypoGraph is a subclass 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"))
        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 won't 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 won't 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 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 won't 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.

    def update(self, count):
        """
        Called periodically by a timer to update the count.
        """
        self.count = count

        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"))
                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 we've 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 don't use the complete range, but step directly from 100 (fairly dark) to 228 (quite bright). The step is computed by 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. There's 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):

    def __init__(self, docmanager, workspace, w, h, *args):
        apply(QWidget.__init__, (self,) + args)

        self.docmanager = docmanager
        self.workspace = workspace

        self.resize(w, h)
        self.setMinimumSize(w,h)
        self.setMaximumSize(w,h)

        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.

    def addGraph(self, document, view):
        self.currentGraph = TypoGraph(0,
                                      self.h,
                                      self.w)
        self.graphMap[document] = (self.currentGraph, 0)
        self.currentDocument = document

    def changeGraph(self, view):
        self.currentGraph = self.graphMap[view.document()][0]
        self.currentDocument = view.document()
        bitBlt(self, 0, 0,
               self.currentGraph,
               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)

        delta = newCount - prevCount

        if delta < 0: delta = 0 # no negative productivity

        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 Python's 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.

    def paintEvent(self, ev):
        p = QPainter(self)
        bitBlt(self, 0, 0,
               self.currentGraph,
               0, 0,
               self.w,
               self.h)

class TestWidget(QWidget):

    def __init__(self, *args):
        apply(QWidget.__init__, (self,) + args)
        self.setGeometry(10, 10, 50, 250)
        self.pixmap = TypoGraph(0, self.width(), self.height())
        self.timer = self.startTimer(100)

    def paintEvent(self, ev):
        bitBlt(self, 0, 0, self.pixmap, 0, 0, self.width(), self.height())

    def timerEvent(self, ev):
        self.pixmap.update(whrandom.randrange(0, 300))
        bitBlt(self, 0, 0, self.pixmap, 0, 0, self.width(), self.height())

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 it's 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:

    def slotSettingsTypometer(self, toggle):
        if toggle:
            self.typowindow = TypoMeter(self.docManager,
                                        self.workspace,
                                        100,
                                        100,
                                        self,
                                        "type-o-meter",
                                        Qt.WType_TopLevel or Qt.WDestructiveClose)
            self.typowindow.setCaption("Type-o-meter")
            self.typowindow.show()
        else:
            self.typowindow.close(TRUE)

Destroying this popup-window is important, because you don't want to waste processing power on a widget that still exists and is merely hidden. The character picker popup we will create in the next section will be hidden, not destroyed.

Prev | Home | Next
Conclusion | Up | QCanvas


Gui Design in the Baroque Age

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Gui Design in the Baroque Age
GUI Programming with Python: QT Edition
Prev Next

Chapter 22. Gui Design in the Baroque Age

Table of Contents
Types of gui customization
Faking it with bitmaps
Creating themes with QStyle

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.

Types of gui customization

Despite the slight banter in the introduction, there are many valid reasons to design a gui for your application that diverges from the platform standard. For instance, while a word processor shouldn't look like the desk of a medieval scribe, the paste-board gui used in an application like Adobe FrameMaker is very effective, especially since many layout designers are still familiar with the glue-and-scissors approach of creating a layout. Likewise, designing an mp3-player that looks a bit like an audio rack is effective (while making it look like an angry fish-eye is less so). Generally speaking, creating an interface that matches closely the real world experience of your users is a good idea.

There is another area where a custom gui would fit well: the general redesign, or theming of the whole set of widgets, to make them more esthetically pleasing to your users. Qt already offers a selection of those themes. Your user can decide whether they want their applications to look like Motif, CDE, Silicon Graphics IRIX, Windows 95 or MacOS 9. Offering this choice means that the applications you make can always blend in unobtrusively with the other applications your users use. A toolkit like Tkinter does not offer this advantage; a user will always be aware that he or she is not working with a application native to his computing 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 that's more complicated than this.)

Prev | Home | Next
Conclusion | Up | Faking it with bitmaps


Drag and drop

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Drag and drop
GUI Programming with Python: QT Edition
Prev Next

Chapter 23. Drag and drop

Table of Contents
Handling drops
Initiating drags
Conclusion

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 Drag'n'Drop 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.

Handling drops

Our Kalam editor must be able to accepts drops from other applications, so we will have to implement this functionality.

Incoming drop operations are easy enough to handle. Basically, you call self.setAcceptDrops(TRUE) for the widget or widgets that should accept drop events, and then actually handle those drop events.

The widget that should accept the drops is our KalamView class. But setting self.setAcceptDrops(TRUE) in the constructor of KalamView won't work. This is because KalamView doesn't actually handle the drops; rather, they are handled by the QMultiLineEdit class, which is encapsulated by KalamView.

The easiest solution is to extend the small subclass of QMultiLineEdit (which we already created) to handle drop events.

Example 23-1. Handling drop events

"""
kalamview.py - the editor view component for Kalam

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE
class KalamMultiLineEdit(QMultiLineEdit):

    def __init__(self, *args):
        apply(QMultiLineEdit.__init__,(self,) + args)
        self.setAcceptDrops(TRUE)

    def dragEnterEvent(self, e):
        e.accept(QTextDrag.canDecode(e))

    def dropEvent(self, e):
        t=QString()
        if QTextDrag.decode(e, t): # fills t with decoded text
            self.insert(t)
            self.emit(SIGNAL("textChanged()"), ())

    def event(self, e):
        if e.type() == QEvent.KeyPress:
            QMultiLineEdit.keyPressEvent(self, e)
            return TRUE
        else:
            return QMultiLineEdit.event(self, e)

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 we'd 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 the operation was successful, and the result of the operation. In the case of QFontDialog, the results are returned in a tuple:

        (font, ok) = QFontDialog.getFont(self, "FontDialog")

Likewise, QTextDrag.decode wants to return two values: a boolean and a string. However, here you need to first create a QString, and pass that to QTextDrag.decode, which fills it with the text, while returning a boolean indicating whether the decoding went well.

However, having got the text by hook or by crook, we can insert it in the view, and tell the world that the text has changed (so other views on the text can update themselves).

You can now select and drag that text to a Kalam document. If you drag a file, only the filename will be inserted, because a filename can also be decoded as text. If you want to open the file instead, you still have some work to do.

Prev | Home | Next
Creating themes with QStyle | Up | Initiating drags


Printing

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Printing
GUI Programming with Python: QT Edition
Prev Next

Chapter 24. Printing

Table of Contents
The QPrinter class
Adding printing to Kalam
Putting ink to paper
Conclusion

Printing: a subject that strikes fear into the heart of even the most stalwart developer—and with good reason. Getting a user's 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 work—the layout of the page for paper—is 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.

The QPrinter class

Generally speaking, there will be only one active printer attached to a user's system. This means that we will keep a single reference to a QPrinter object. Therefore, we might as well create the QPrinter in the __init__() method of the KalamApp class.

# kalamapp.py

class KalamApp(QMainWindow):
    """KalamApp is the toplevel application window of the kalam unicode editor
    application.
    """
    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:

The printer setup dialog

Prev | Home | Next
Conclusion | Up | Adding printing to Kalam


Internationalizing an Application

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Internationalizing an Application
GUI Programming with Python: QT Edition
Prev Next

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. That's nonsense: it's 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 Python's 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. PyQt's 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.

Translating screen texts

The first task is to surround all translatable text with the method self.tr() - every QObject - derived class has that method. You don't have to do that manually with designs you have generated with the Designer module or Qt Designer. However, for Kalam, it's a fair bit of work - I'll only show a fragment here:

# fragment from kalamapp.py
 ...
    def initActions(self):
        self.actions = {}
        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 won't 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 don't have to worry about performance loss.

After you've marked all translatable strings, you can use a utility to generate translatable message files. Qt's utility—either lupdate or findtr—can only work with strings marked with tr(), and only with double-quoted strings.

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() doesn't 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:

pyuic -tr _ frmsettings.ui

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:

boud@calcifer:~/doc/pyqt/ch19/kalam > pygettext.py --keyword=tr kalamapp.py

Which creates a file called messages.pot, or:

boud@calcifer:~/doc/pyqt/ch19/kalam > findtr kalamapp.py

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 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')

Or for message catalogs in the Unicode encoding:

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.

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

And run the following command:

boud@calcifer:~/doc/pyqt/ch19/kalam > lupdate kalam.pro

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.

The Qt Linguist screen

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:

Example 25-1. Installing the translator

#!/usr/bin/env python
"""
main.py - application starter

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
import sys, locale

from qt import *

from kalamapp import KalamApp
from kalamdoc import KalamDoc
from kalamview import KalamView
import kalamconfig

from resources import TRUE, FALSE

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.

Prev | Home | Next
Conclusion | Up | Delivering your Application


Delivering your Application

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Delivering your Application
GUI Programming with Python: QT Edition
Prev Next

Chapter 26. Delivering your Application

Table of Contents
Introduction
Packaging source
Starting with distutils.
Creating Unix RPM packages
Windows installers
Desktop integration

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 McMillan's Installer, Fredrik Lundh's Squeeze (which is packaged with the PythonWorks IDE), and finally Thomas Heller's 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 McMillan's 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.

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, it's difficult to achieve even a little integration with the user's 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 menu's). 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, it's a matter of forcing your users to read the manual. I don't describe the process of packaging your own C or C++ extensions, though it is possible. Consult the Distutils manual for more information.

Prev | Home | Next
Internationalizing an Application | Up | Packaging source


Envoi

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Envoi
GUI Programming with Python: QT Edition
Prev Next

Chapter 27. Envoi

And that's 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 Qt's QTable. I would have liked a chapter dedicated to QEvent and QEventLoop. More discussion of Qt's 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 don't want to stop writing—the 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.

Prev | Home | Next
Desktop integration | Up | Appendices


Preface
Prev Next

Preface

Table of Contents
Who is using PyQt
For whom is this book intended
How to read this book
Conventions
Acknowledgments

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 thirty–somethings who started programming in their teens, I've 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. I've done my stretch with Oracle Forms, and served as a Java developer. On Linux, I've 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 there's always something new to learn or explore in Python, if you're in the mood, it's 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 I'd 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.

Joshua's 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 don't hide the complexity that is a part of any programming effort.

Both are right, of course, so I decided to give you both small examples and one really large one. Part I and II of this book concern themselves with concepts: here the examples are small, often amounting to less than one page of code. Part III takes you through the development of a complete, complex application. In this case an editor, but one with a lot of extra features. I think it's a very good way of learning what developing complex applications entails - I spare you none of the nasty details that software development entails.

I have tried to keep to a very clear style of coding, with few or none of the clever hacks that are possible in Python — like adding the methods of one class to another, or creating lists of function objects. The purpose is to tell you about writing real applications using Python and Qt. Clever hacking has its place, but is best savored on its own.

The emphasis of the book is also firmly on application development, not on creating graphics per se — although several techniques are mentioned here and there that have to do with creating charts and graphs.

Who is using PyQt

The combination of Python and Qt is extremely powerful, and is used in a wide variety of applications. People are scripting OpenGL applications with it, creating complex 3D models, animation applications, writing database applications, games, utilities and hardware monitoring applications. It is used in open source projects, but also by large companies, like Disney Television and Media. If you're not working on embedded software, hardware drivers or a new operating system, chances are that PyQt is the right choice for you, too.

Prev | Home | Next
GUI Programming with Python: QT Edition |  | For whom is this book intended


Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Index
GUI Programming with Python: QT Edition
Prev

Index

Prev | Home
Bibliography |


Introduction to the BlackAdder IDE

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Introduction to the BlackAdder IDE
GUI Programming with Python: QT Edition
Prev Next

I. Introduction to the BlackAdder IDE

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 I'll 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.

Table of Contents
2. Installation
3. Interface
4. Introduction to Python
5. Debugging
Prev | Home | Next
About the BlackAdder IDE |  | Installation


PyQt fundamentals

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

PyQt fundamentals
GUI Programming with Python: QT Edition
Prev Next

II. PyQt fundamentals

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.

Table of Contents
6. Qt Concepts
7. Signals and Slots in Depth
8. String Objects in Python and Qt
9. Python Objects and Qt Objects
10. Qt Class Hierarchy
11. Qt Designer, BlackAdder and uic
Prev | Home | Next
If all else fails |  | Qt Concepts


Creating real applications with PyQt

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Creating real applications with PyQt
GUI Programming with Python: QT Edition
Prev Next

III. Creating real applications with PyQt

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 won't use every single widget that PyQt offers us.

Table of Contents
12. Application Frameworks
13. Actions: menus, toolbars and accelerators
14. Automatic testing with PyUnit
15. A More Complex Framework: Multiple Documents, Multiple Views
16. User Interface Paradigms
17. Creating Application Functionality
18. Application Configuration
19. Using Dialog Windows
20. A Macro Language for Kalam
21. Drawing on Painters and Canvases
22. Gui Design in the Baroque Age
23. Drag and drop
24. Printing
25. Internationalizing an Application
26. Delivering your Application
27. Envoi
Prev | Home | Next
Advanced Designer topics |  | Application Frameworks


Appendices

Your OpenSource Publisher™
Products | Where to buy | Retailers | FAQ | Write for Us. | Contact Us.

Boudewijn Rempt

ISBN: 0-97003300-4-4
Available from bookstores everywhere or you can order it here.

You can download the source files for the book (code / eps) here.

Appendices
GUI Programming with Python: QT Edition
Prev Next

IV. Appendices

Table of Contents
A. Reading the Qt Documentation
B. PyQwt: Python Bindings for Qwt
C. First Steps with Sip
Prev | Home | Next
Envoi |  | Reading the Qt Documentation


sipParseArgs
Prev Next

sipParseArgs

Name

sipParseArgs -- Parse the arguments to a C/C++ function without any side effects

Synopsis

int sipParseArgs(int * argsParsedp, PyObject * sipArgs, char * fmt, ...);

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.

Table C-1. Error flags in sipParseArgs()

Flag Name | Meaning
PARSE_OK | Parse is Ok so far
PARSE_MANY | Too many arguments
PARSE_FEW | Too few arguments
PARSE_TYPE | Argument with a bad type
PARSE_MASK | Mask covering the error flag bits

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:

Table C-2. Format specifiers for sipParseArgs()

fmt | Operand type | expected C argument(s)
s | String or None | char **
S | Slot name, return the name | char **
G | Signal name, return the name | char **
I | Class instance | int (*convfunc)(PyObject *), PyObject **
O | Python object of any type | PyObject **
T | Python object of given type | PyTypeObject *, PyObject **
R | Sub-class of QObject | PyObject **
F | Python callable object | PyObject **
a | Byte array or None | char **, int *
c | Character | char *
i | Integer | int *
h | Short integer | short *
l | Long integer | long *
f | Float | float *
d | Double float | double *
v | Void pointer | void **

...

A variable number of pointers to the arguments which will take the parsed values.

Examples

Example C-1. Interface for QRegExp::match

// Attempts to match in str, starting from position index.
// Returns the position of the match, or -1 if there was no match.
// if len is not a null pointer, the length of the match is stored in *len.
int match(const char* str, int index=0, int*=0) const;
%MemberCode
    // The Python interface returns the position and length as a tuple.
    const char *str;
    int index = 0;

    if (sipParseArgs(&sipArgsParsed, sipArgs, "s|i", &str, &index))
    {
        int pos, len;
        QRegExp *ptr;

        if ((ptr = (QRegExp*) sipGetCppPtr(sipThis, sipClass_QRegExp)) == NULL)
            return NULL;
        pos = ptr -> QRegExp::match(str, index, &len);
        return Py_BuildValue("(ii)", pos, len);
    }
%End

Prev | Home | Next
SIPLIB Functions | Up | sipConvertToCpp


For whom is this book intended
Prev Preface Next

For whom is this book intended

This is the first book on Python and Qt. There have been quite a few books on C++ and Qt, but you would need to be fairly adept at mentally searching and replacing C++ language constructs to be able to use those books for pleasure and profit if 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 don't want to translate C++ to Python to figure out what they are supposed to do.

This is the first group of people for whom I've 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 language—there 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 you've 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.

Prev | Home | Next
Preface | Up | How to read this book


How to read this book
Prev Preface Next

How to read this book

Like ancient Gaul, this book is divided in three parts. The first part details the installation of PyQt and of BlackAdder. Then the book takes you through a tour of the interface of BlackAdder. You might want to read this part in order. There is also 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 don't 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 doesn't 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 don't 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.

Prev | Home | Next
For whom is this book intended | Up | Conventions


Conventions
Prev Preface Next

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 don't quote the parameter list. This makes it easier to follow the run of the text

Even though PyQt is a cross-platform toolkit (and I've 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' Howl's Moving Castle. Because BlackAdder wasn't ready when I wrote this book, I used XEmacs and Bash (the command line shell) to create and test all examples. That's why you will often see prompts in illustrations, and not so often screenshots of BlackAdder:

boudewijn@maldar:~/doc/pyqt/ch3 > python sigslot.py
Object with ID 135113236 Got signal: message
Object with ID 135115668 Got signal: message
Object with ID 135318532 Got signal: message
boudewijn@maldar:~/doc/pyqt/ch3 >

If you are using Windows, you can use a DOS box to emulate the Bash shell, but it won't 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, it's been long years since I last watched those on the television, and I don't feel quite up to it. I've done my best, but don't 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 you'll find versions of the examples both for Qt 2.x and Qt 3.x.

Prev | Home | Next
How to read this book | Up | Acknowledgments


Acknowledgments
Prev Preface Next

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 I'm inclined to agree, but we'll 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 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. I've 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.

Prev | Home | Next
Conventions | Up | Introduction


GUI programming with Python
Prev Chapter 1. Introduction Next

GUI programming with Python

One area where you do want the snappiest response possible is your user interface. Users are notoriously impatient creatures, and they are right. Responsiveness is important. Likewise, conformance to platform standards is important, as is a well thought-out programming model, to make your life easier. You want to have as little GUI code to as possible, because that means that there are less opportunities for bugs. With these criteria, we can set out to select a good GUI toolkit.

That it is possible at all to select a GUI toolkit might come as a bit of a surprise to Visual Basic developers, who cannot choose, but have to use whatever Microsoft provides. There is a cornucopia of GUI toolkits available for Unix/X11. Because Python is so easily extensible with C and C++ modules, a large part of them is usable, ‘bound to' is the technical term, from Python. Quite a few of those toolkits are available on Windows too. Because all computer intensive drawing and interaction code runs in native machine code, outside the Python virtual machine, the interface can be as responsive as the interface of an application written in C or C++.

The following GUI toolkits exist for Python:

Table 1-1. GUI Toolkits for Python

Gui Toolkit | Windows | Unix/X11 | MacOS | Notes
Tkinter | Yes | Yes | Yes, mostly | Tkinter is the most ancient Python GUI 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 Grayson's book, Python and Tkinter programming.
PyQt | Yes | Yes | OS X only | PyQt is based on Qt, the cross-platform GUI toolkit by Troll Tech. It's 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. FxPy's main feature is execution speed.
PyGTK (+PyGnome) | Yes (a bit) | Yes | (If you run a separate X Server on OS X) | PyGTK is based on GTK (formerly known as the Gimp Toolkit). Not really intended for cross-platform work, it has recently been ported (more or less) to Windows.
Pythonwin | Yes | No | No | Pythonwin is the - rather underdocumented - binding to Microsofts MFC library. It's not portable, of course.

There are many others GUI toolkits available, both dead and alive. For a complete listing, please see Cameron Laird's notes on Python GUI's 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, programming model, completeness of the assortment of widgets and ease of installation. Oh, and because it was the most fun to use, of course!

There were other considerations, of course. Tkinter is often very slow - try running the IDLE IDE that comes with Python. In contrast, PyQt is very snappy. The Tcl-tk programming model that Tkinter is based on doesn't translate as well to Python as the modified C++ programming model of PyQt. PyQt has also been very well designed: I just love the signal/slot mechanism of PyQt. There is also just about every type of widget I need, and PyQt is easy to install. WxPython, because it's a library (wxPython) based on a library (wxWindows) based on a library (MFC or GTK) can be really difficult to get up and running. Finally, the GUI designer in BlackAdder (or the free equivalent Qt Designer) is a strong point in favor of Qt, too.

The most important features of PyQt are:

  • Based on Trolltechs C++ Qt toolkit.

  • Runs on Windows and Unix/X11 (and soon on Apple's OS X)

  • Uses the innovative signals/slots paradigm to couple GUI items and actions.

  • Binds almost the complete Qt library

  • Allows subclassing of Qt classes in Python

  • Allows applications to mimic the look and feel of Windows, Motif, CDE, SGI and MacOS 9, or take on a custom look and feel.

  • Comes with an enormous inventory of advanced GUI controls, such as a canvas, an editable table module and a rich text editor (in version 3.0).

Prev | Home | Next
Introduction | Up | About the BlackAdder IDE


About the BlackAdder IDE
Prev Chapter 1. Introduction Next

About the BlackAdder IDE

BlackAdder is growing into a rather nice development environment for Python and PyQt applications. You can use it to rapidly prototype interfaces, or to develop complete, complex applications in a very short time.

The central feature of BlackAdder is the editor, which is specially geared towards working with Python, and includes folding, syntax highlighing and auto-indent. 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 you've 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 doesn't force you to use PyQt - you can just as well create a Tkinter application with BlackAdder. You won't 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.

Prev | Home | Next
GUI programming with Python | Up | Introduction to the BlackAdder IDE


Installing sip and PyQt without BlackAdder
Prev Chapter 2. Installation Next

Installing sip and PyQt without 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 Linux—and particularly if you use a modern and complete distribution like SuSE or Redhat—everything 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 don't 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!

Building from source on Linux

Assuming you already have an installation of Python and Qt, you first need to gather the following packages if you want to build PyQt from source:

  • 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.

You need to compile and install sip before compiling PyQt. After unpacking the sip tar archive, you will need to give the following commands:

        boud@calcifer:~/src/sip-2.5 > ./configure; make

And, when building is complete, become superuser and type:

        root@calcifer:/home/boud/src/sip-2.5 > make install

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:

        boud@calcifer:~/src/sip-2.5 > ./configure \
        --with-qt-dir=/opt/qt \ --with-python=/usr/local/bin/python

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 isn't the fastest around.

        boud@calcifer:~/src/PyQt-2.5 > ./configure; make ...
        boud@calcifer:~/src/PyQt-2.5 > su Password:
        root@calcifer:/home/boud/src/PyQt-2.5 > make install

The whole process might take a while, but should not pose any problem. Sometimes, however, things go wrong...

Problems with compilation

While not really difficult, compiling from source is an exercise to be attempted only by the technically adept. Certain problems have cropped up throughout the life of PyQt, only to be solved in later versions. Other problems have been caused by the version of the GNU C++ compiler used in certain releases of Redhat: gcc 2.96.

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. Murphy's 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.

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:

  • The Non-commercial licensed Qt library from http://www.trolltech.com. This a full version of Qt. You cannot build commercial applications with this library, and you cannot use your software in a commercial setting. Additionally, you must make the source to your software available. See http://www.trolltech.com/products/download/freelicense/noncommercial-dl.html for more information.

  • You also need the precompiled PyQt for Windows library from http://www.thekompany.com/projects/pykde/. This is made available under the same license as the non-commercial version of Qt. You don't need to download a separate copy of sip.

  • Of course, Python is needed, too! Get it from http://www.python.org.

You also need a nice editor to type your Python code with. notepad just won't do - 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.)

The SciTE editor.

You job is then reduced to manually installing Python, Qt, PyQt, and an editor. Then you can get started on developing your application.

Prev | Home | Next
Installation | Up | Interface


Toolbars
Prev Chapter 3. Interface Next

Toolbars

BlackAdder possesses an imposing array of toolbars. I'd certainly advise you to not emulate this example in your own application!

The BlackAdder collection of toolbars.

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 let's 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:

File toolbar

This toolbar contains buttons that can be used to create, open and save BlackAdder projects, single Python scripts and dialog designs. The last two buttons are used to compile either the current dialog design, or all dialog designs in the current project. In this case, compilation means that the XML-based designs are converted to usable Python code. Running a project also means that all form designs will be compiled to Python.

The file toolbar.

Edit toolbar

Undo, redo, cut, copy and paste are all fairly standard functions, and work on both widgets in dialog design mode, and on text in the editor.

The edit toolbar.

Execution toolbar

This toolbar contains buttons to start scripts and projects, debug scripts and projects, restart debugging, continue running while debugging, single stepping, clear the breakpoints in the currently edited script, clear all breakpoints set in the whole project, and cancel or stop debugging.

The execution toolbar.

Layout manager toolbar

On the next row we find the layout manager toolbar. Again, from left to right, there are buttons to resize widgets, insert a horizontal, vertical and grid layout manager, remove the current layout manager and insert a spacer object (the spacer is the the curious springy thing).

The layout toolbar.

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.

The first widget toolbar.

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, you'll discover that I haven't been lying to you in this section.

The help toolbar.

Pointer toolbar

The first toolbar on the third row has three buttons. The first resets your cursor to a pointer. This is useful if you want to select a widget in your design, but have previously pressed another button. The second allows you to graphically connect signals and slots between elements on screen. The last button helps you put all the components of your design in the right tab order.

The pointer toolbar.

More widget toolbars

The last four toolbars contain more widgets: pushbutton, toolbutton, radio button and checkbox. Then we have group box, button group, frame and tab-bar, next listbox, treeview, iconview and table. Finally you get line-editor, spin box, multi-line editor, slider and dial.

The widget toolbars.

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.

Prev | Home | Next
Interface | Up | Project management


Project management
Prev Chapter 3. Interface Next

Project management

A BlackAdder project is a simple entity: a grouping of Python scripts and GUI form designs, and perhaps a few extraneous files and folders. You start a project by choosing New Project from the File menu.

Creating a new project.

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.

Adding a file to a project.

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.

The Project Explorer in BlackAdder.

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).

Prev | Home | Next
Toolbars | Up | BlackAdder Configuration


BlackAdder Configuration
Prev Chapter 3. Interface Next

BlackAdder Configuration

By choosing Edit - Preferences you open the preferences dialog window. Here you can alter BlackAdder's configuration options.

The configuration dialog.

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 don't 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.

Decent editor settings.

Prev | Home | Next
Project management | Up | Editing


Editing
Prev Chapter 3. Interface Next

Editing

The BlackAdder editor is based on Neil Hodgson's 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 weren't integrated in the beta version of BlackAdder I used to write this book.

Prev | Home | Next
BlackAdder Configuration | Up | Python shell


Python shell
Prev Chapter 3. Interface Next

Python shell

As a final feature, BlackAdder offers a Python shell in a separate window. This is mainly useful for quickly trying out certain ideas. You activate it by choosing Python Interpreter in the Window menu.

Python shell window.

BlackAdder doesn't 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.

Prev | Home | Next
Editing | Up | Conclusion


Conclusion
Prev Chapter 3. Interface Next

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.

Prev | Home | Next
Python shell | Up | Introduction to Python


The Rules
Prev Chapter 4. Introduction to Python Next

The Rules

For a full treatment of the rules of Python, see the Python Language Reference, which is available online with BlackAdder and Python. This section will in a series of short statements enumerate what makes Python Python.

Objects and references

Before Python 2.2, not all types were classes, but now they are.

Moore's law has made type declarations obsolete (with thanks to Paul Prescod).

An object has a type (which you can query with type()). A reference does not have a type. You can use the same name to refer to two objects in succession, but the first reference disappears as soon as you've made the second.

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 doesn't 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
>>>

Functions and classes are both also objects.

Every object has one identity, which you can retrieve with id():

>>> 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__.

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 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.

Python 2.1.1 (#1, Aug 11 2001, 20:14:53)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> class A:
...     def __test():
...             pass
...
>>> dir(A)
['_A__test', '__doc__', '__module__']
>>> a=A()
>>> dir (a)
[]

Keywords

The following keywords are reserved:

and       del       for       is        raise
assert    elif      from      lambda    return
break     else      global    not       try
class     except    if        or        while
continue  exec      import    pass      yield
def       finally   in        print

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.

Methods and functions

Functions are callable objects that return a value (if a function doesn't explicitly return a value, it retuns None). Methods are the same, but part of a class. A method's argument list always has self (which refers to the class instance) as its first argument.

A function can be called with positional arguments, or named arguments. When mixed, positional arguments come first.

A variable number of positional arguments is indicated by *args, and a variable number of named arguments is indicated by **args. You can access *args as a tuple in your function, and **args as a dictionary in your function.

>>> def f(a):
...     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}
>>>

High level datatypes

Python has three very high level datatypes: tuples, lists and dictionaries.

A tuple is any combination of unique objects. You can't change the composition of items in a tuple (i.e. substitute another object), although the objects themselves can be changed.

>>> t=("a","b","c")
>>> t
('a', 'b', 'c')
>>> t[2]="d"
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: object doesn't support item assignment
>>>

A list is a list of objects. You can change which objects are in a list, adding and deleting items to your heart's delight.

>>> l=["a", "b", "c"]
>>> l[2]="d"
>>> l
['a', 'b', 'd']
>>>

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.

>>> d={"a": "aaa", "b": "bbb", "c": "ccc"}
>>> d
{'b': 'bbb', 'c': 'ccc', 'a': 'aaa'}
>>> d[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
KeyError: 2
>>> d["b"]
'bbb'
>>> d["b"]="ddd"
>>> d
{'b': 'ddd', 'c': 'ccc', 'a': 'aaa'}
>>>

Prev | Home | Next
Introduction to Python | Up | Constructions


Constructions
Prev Chapter 4. Introduction to Python Next

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.

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:

Python 2.1.1 (#1, Aug 11 2001, 20:14:53)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> aList=["a","b","c"]
>>> for item in aList:
...     print item
...
a
b
c
>>> for counter in range(3):
...     print counter
...
0
1
2
>>>

Another loop repeats a block of statements while a certain expression evaluates to true:

>>> 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.

Iterators define a __iter__ and a next() function to allow easy looping:

# iter.py - an iterator

class MyIterator:

    def __init__(self, start):
        self.start = start

    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

from __future__ import generators

def MyGenerator():
    count = 0
    while count < 10:
        yield count
        count += 1

gen = MyGenerator()
try:
    while 1:
        print gen.next()
except StopIteration:
    print "finished"

Note how yield returns the number, but count is still increased.

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:

>>> from qt import *
>>> a=QString("bla")
>>> b=QString("bla")
>>> a is b
0
>>> c=a
>>> a is c
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.

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:

>>> class BlaError:
...     def __init__(self, value):
...             self.value = value
...     def __str__(self):
...             return repr(self.value)
>>> try:
...     raise BlaError("Bla happened - that's bad!")
... except BlaError, error:
...     print error
...
Bla happened - that's 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 "that's 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

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
...
>>> class B(A):
...     def __init__(self, val):
...             self.val = val
...     def __str__(self):
...             return str(self.val)
...
>>> b=B(10)
>>> print b
10
>>>

Prev | Home | Next
The Rules | Up | Conclusion


Conclusion
Prev Chapter 4. Introduction to Python Next

Conclusion

This concludes a very short tour of Python. There is much more to the language, but this chapter has described the basis. It's 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.

Prev | Home | Next
Constructions | Up | Debugging


Setting breakpoints
Prev Chapter 5. Debugging Next

Setting breakpoints

Setting a breakpoint in your script is done by clicking on the left vertical gray bar in 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.

Setting a breakpoint (the small white circle left to ‘print i').

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, I'll 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. Let's 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):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.setCaption("Button")

        self.grid=QGrid(2, self)
        self.grid.setFrameShape(QFrame.StyledPanel)

        self.bn=QPushButton("Hello World", self.grid)
        self.bn.setDefault(1)

        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 you've 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.

Prev | Home | Next
Debugging | Up | Stepping along


Stepping along
Prev Chapter 5. Debugging Next

Stepping along

If your application goes wrong in the initialization phase, then you might want to single step every statement until you arrive at the offending statement. Let's debug the same script as in the previous section, but altered slightly to introduce a pesky typo. If you can spot it from eyeballing the code alone, then you've probably skipped ahead to Chapter 7, on signals and slots.

#
# button2.py
#

from qt import *
import sys

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.setCaption("Button")

        self.grid=QGrid(2, self)
        self.grid.setFrameShape(QFrame.StyledPanel)

        self.bn=QPushButton("Hello World", self.grid)
        self.bn.setDefault(1)

        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 won't place the window on screen. Instead, it ends with the following stacktrace:

Traceback (most recent call last):
  File "<stdin>", line 37, in ?
  File "<stdin>", line 30, in main
  File "<stdin>", line 21, in __init__
  File "<stdin>", line 26, in slotSlot
ZeroDivisionError: integer division or modulo by zero

If you single step this using BlackAdder you'll notice that directly after the line:

        self.connect(self.bn, SIGNAL("clicked()"),
                     self.slotSlot())

Python continues with:

    def slotSlot(self):

Accidentally stepping into a function.

Armed with this knowledge, it's easy to see what went wrong: we called the function in the connect line by adding the two brackets, passing the result of the slotSlot() function to the connect(), instead of giving the function object as a parameter to connect. Simply changing the line to:

        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 function—this is something to be aware of when creating a very dynamic application.

Prev | Home | Next
Setting breakpoints | Up | Debugging Techniques


Debugging Techniques
Prev Chapter 5. Debugging Next

Debugging Techniques

Once you know your debugger, the real work can begin. Here's a short overview of debugging techniques that I have found useful.

Avoid changing your code

The temptation to change the code is almost too strong to resist. Perhaps your application doesn't work, and you don't have a solid idea of where the problem is, but only a hunch. So you start changing code. Suddenly you notice another problem—and so you change more code. Nothing works, so you change some more code...

Eventually, the original bug disappears — or the application simply crashes before it reaches the bug!

So, the golden rule is: ‘Don't change code at random'. Indeed don't change any code at all before you know exactly where the problem is. (I'm not talking about a sprinkling of print statements here, of course.)

There's another point to the maxim ‘don't change your code': you should debug problems now, and not when you've finished writing the application. If you don't fix a bug as soon as you come across it, you'll probably never fix it. And bugs don't disappear because you added more code.

Gather data

Try to determine the pattern of failures. This pattern might be caused by data or by usage patterns. That's why it is so important to have other people test your software (a cat walking on a keyboard is an excellent input simulator!). You know all the ‘right' paths through your code, so you'll probably never bring it down. Experienced users know those paths, too, which is why software appears to become more stable after people have been using it for a longer time.

Minimal examples

Sometimes you simply want to know whether a certain Python construction works at all. Nobody is perfect, and everyone has little things they are perpetually unsure about. Mine is string slicing. I never know what exactly will be returned by "bla.bla"[:-4].

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:

I wonder what happens when...

Prev | Home | Next
Stepping along | Up | If all else fails


If all else fails
Prev Chapter 5. Debugging Next

If all else fails

If you can't nail that pesky bug down, it helps to confess all. Go to a colleague, to your partner in health and sickness, or, at a pinch, to your cat, and tell them about the bug. You probably won't even have to finish your explanation—before you've done so, you'll have seen the light yourself.

However, even if consulting your cat does not avail you of the right solution, you might have finally stumbled onto a problem with PyQt or with Qt. In all the years that I've been developing with Python I have not come across a single bug in Python, and, what's more, I've 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 hasn't 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.

Prev | Home | Next
Debugging Techniques | Up | PyQt fundamentals


As simple as they come
Prev Chapter 6. Qt Concepts Next

As simple as they come

Nothing like getting the feet wet: let's investigate the structure of a PyQt application by putting together the minimal set of components that can show something on screen, slowly expanding it to show more features.

A tiny PyQt applications has the following elements:

  • 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

Example 6-1. hello1.py — hello world

#
# hello1.py
#
import sys                                                   (1)
from qt import *                                             (2)

app=QApplication(sys.argv)                                   (3)
button=QPushButton("Hello World", None)                      (4)
app.setMainWidget(button)                                    (5)
button.show()                                                (6)
app.exec_loop()                                              (7)
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.

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 doesn't 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, it's 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 doesn't 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)
Then we show() the button to the world.
Until the application objects starts the event loop, nothing will appear on screen. The call app.exec_loop() does return a value, but we can safely disregard it.

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:

b = Button(root, text=label, command=func)

where PyQt wants:

b = QPushButton(root, label, func)

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.

Prev | Home | Next
Qt Concepts | Up | A better Hello World


A better Hello World
Prev Chapter 6. Qt Concepts Next

A better Hello World

Of course, you will never write a script like the previous one in earnest. While it works, it doesn't even show the correct way of setting up a PyQt application. A far superior structure is as follows:

Example 6-2. hello2.py — a better hello world

import sys
from qt import *

class HelloButton(QPushButton):

    def __init__(self, *args):
        apply(QPushButton.__init__, (self,) + args)
        self.setText("Hello World")

class HelloWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.button=HelloButton(self)
        self.setCentralWidget(self.button)

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 can't 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 problem—try 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 QMainWindow is designed for. We didn't define any menu items, so the window is still a bit bare.

The central part of a window—the letterbox, so to speak—is 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.

A better hello world

You set the central part of the window with the setCentralWidget() method:

self.setCentralWidget(self.button)

An application can have zero, one, or more windows — and an application shouldn't close down until the last window is closed. QApplication keeps count of the number of windows still open and will try to notify the world when the last one is closed. This is done through the signals/slots system. While this system will be discussed in depth in a later chapter, it's sufficiently important to warrant some consideration here.

Basically, objects can register an interest in each other, and when something interesting happens, all interested objects are notified. In this case, the QApplication object wants to know when the last window is closed, so it can quit.

app.connect(app, SIGNAL("lastWindowClosed()"),
            app, SLOT("quit()"))

Let's 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:

Example 6-3. fragment from hello3.py

...
class HelloWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.button=HelloButton(self)
        self.setCentralWidget(self.button)
        self.connect(self.button, SIGNAL("clicked()"),
                     self, SLOT("close()"))

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 closes—and we have our first interactive PyQt application!

An interesting exercise is to create more than one window by rewriting the main function:

Example 6-4. Fragment from hello5.py

...
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()
...

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):

Example 6-5. Fragment from hello4.py

...
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()
...

and see what happens...

This is one of the interesting features in Python: in contrast to C++, Python has a garbage collector. (Actually, you can choose between a garbage collector and a reference counter, but I don't want to get that technical yet). This virtual garbage-man will remove unreferenced objects as soon as possible. That means that any object that doesn't have a Python variable name associated with it will disappear. (Unless the object is the child of a QObject; see Chapter 9 for all the details). If you were to try this trick in C++, keeping references would make no difference, as C++ does not delete unused objects for you, which can easily lead to nasty memory leaks.

Prev | Home | Next
As simple as they come | Up | Designing forms


Designing forms
Prev Chapter 6. Qt Concepts Next

Designing forms

One important feature of BlackAdder and PyQt is the visual gui designer. You can use the designer to easily create all kinds of dialog windows, and even custom widgets.

The definition of these user-interface designs is saved in files in an XML format, and you can easily translate those to Python code. The beauty of the system is that you can just as easily translate your designs to valid C++ code, making it easy to develop your prototype in Python and, when satisfied, port the whole gui to fast, compiled C++. (C++ code, I might add, that compiles just as well on Windows, Unix/X11, OS X and embedded systems).

The version of Qt Designer that is included with Qt3 can also create complete main windows with menus and toolbars. Once pyuic has been updated to include those elements, you can use this in your Python projects, too.

Let's use Designer to design a small form that would be useful for connecting to some system, and hook it up in a small PyQt program.

A form in the gui designer

It's 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:

Example 6-6. frmconnect.py

# Form implementation generated from reading ui file 'frmconnect.ui'
#
# Created: Wed Feb 28 21:34:40 2001
#      by: The Python User Interface Compiler (pyuic)
#
# WARNING! All changes made in this file will be lost!

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'))
        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.Expanding)
        Layout5.addItem(spacer)

        frmConnectLayout.addLayout(Layout5,0,1)

        self.grpConnection = QGroupBox(self,'grpConnection')
        self.grpConnection.setSizePolicy(QSizePolicy(5,7,self.grpConnection. \
        sizePolicy().hasHeightForWidth()))

        self.grpConnection.setTitle(self.tr(''))
        self.grpConnection.setColumnLayout(0,Qt.Vertical)
        self.grpConnection.layout().setSpacing(0)
        self.grpConnection.layout().setMargin(0)
        grpConnectionLayout = QGridLayout(self.grpConnection.layout())
        grpConnectionLayout.setAlignment(Qt.AlignTop)
        grpConnectionLayout.setSpacing(6)
        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)
        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 you'll 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:

Example 6-7. dlgconnect.py — the subclass of the generated form

import sys
from qt import *

from frmconnect import frmConnect

class dlgConnect(frmConnect):

    def __init__(self, parent=None):
        frmConnect.__init__(self, parent)
        self.txtName.setText("Baldrick")
        for host in ["elizabeth","george", "melchett"]:
            self.cmbHostnames.insertItem(host)

    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()'),
                    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.

Prev | Home | Next
A better Hello World | Up | Conclusion


Conclusion
Prev Chapter 6. Qt Concepts Next

Conclusion

In this chapter we've taken a first sip of developing with Python and PyQt, touching lightly on many subjects. In the rest of this part, we'll 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.

Prev | Home | Next
Designing forms | Up | Signals and Slots in Depth


Connecting with signals and slots
Prev Chapter 7. Signals and Slots in Depth Next

Connecting with signals and slots

Signals and slots come in two basic varieties: Vanilla, or C++ signals and slots (as defined in the Qt library) and Pythonic (signals and slots defined in Python). Any function of any object can be used as a slot in Python (you don't even have to inherit from QObject). This contrasts to C++, where you need to specially mark a function as a slot in order to be able to connect it to a signal (and have to inherit QObject).

Every class that descends from QObject is eligible for the sending (emitting is the technical term) and connecting of signals to its own methods. That means that if your Python class is to emit signals it has to ultimately inherit QObject.

Connections are made using the connect() method. This is a class method of QObject, and you can, according to your preference, use the method on QObject, or on the actual object you're working with.

You can connect signals to slots, but also to other signals, creating a chain of notifications. If you want to disconnect signals from slots, you can use QObject.disconnect(). If you want to emit signals from a Python object, you can use the QObject.emit() function.

The connect function can take the following parameters:

  • sender — the QObject that will send the signal.

  • signal — the signal that must be connected

  • receiver — the QObject that has the slot method that will be called when the signal is emitted.

  • slot — the slot method that will be called when the signal is emitted.

If you're connecting your signals from within a class, you can often omit the third parameter — the receiver.

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:

Example 7-4. Connecting a signal to a slot

#
# lsv.py - connect a button to a listview
#
import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)

        self.mainWidget=QWidget(self);                       (1)

        self.vlayout = QVBoxLayout(self.mainWidget, 10, 5)

        self.lsv = QListView(self.mainWidget)                (2)
        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"))

        self.bn = QPushButton("Push Me", self.mainWidget)    (3)

        self.vlayout.addWidget(self.lsv)
        self.vlayout.addWidget(self.bn)

        QObject.connect(self.bn, SIGNAL("clicked()"),        (4)
                        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)
We want to combine a pushbutton and a listview in a main window. So we first define a single main widget that can be managed by the layout manager of QMainWindow, and then add a new layout manager to that widget. The pushbutton and the listview then become children of the main widget, self.mainWidget.

You don't 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.
This is the actual connection between the clicked() signal of the button and the invertSelection() of the listview. If you press the button, you'll 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). Let's 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 that's connected to the signal. You need to explicitly enter the types of the signal arguments, but not their names.

Example 7-5. Connection a dial to a label with signals and slots

#
# dial.py — connecting a QDial to a QLabel or two
#
import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)

        self.vlayout = QVBoxLayout(self, 10, 5)
        self.hlayout = QHBoxLayout(None, 10, 5)
        self.labelLayout=QHBoxLayout(None, 10, 5)

        self.red = 0
        self.green = 0
        self.blue = 0

        self.dialRed = QDial(0, 255, 1, 0, self)
        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.labelRed = QLabel("Red: 0", self)
        self.labelGreen = QLabel("Green: 0", self)
        self.labelBlue = QLabel("Blue: 0", self)

        self.labelLayout.addWidget(self.labelRed)
        self.labelLayout.addWidget(self.labelGreen)
        self.labelLayout.addWidget(self.labelBlue)

        self.vlayout.addLayout(self.labelLayout)

        QObject.connect(self.dialRed, SIGNAL("valueChanged(int)"),
                        self.slotSetRed)
        QObject.connect(self.dialGreen, SIGNAL("valueChanged(int)"),
                        self.slotSetGreen)
        QObject.connect(self.dialBlue, SIGNAL("valueChanged(int)"),
                        self.slotSetBlue)

        QObject.connect(self.dialRed, SIGNAL("valueChanged(int)"),
                        self.slotSetColor)
        QObject.connect(self.dialGreen, SIGNAL("valueChanged(int)"),
                        self.slotSetColor)
        QObject.connect(self.dialBlue, SIGNAL("valueChanged(int)"),
                        self.slotSetColor)

    def slotSetRed(self, value):
        self.labelRed.setText("Red: " + str(value))
        self.red = value

    def slotSetGreen(self, value):
        self.labelGreen.setText("Green: " + str(value))
        self.green = value

    def slotSetBlue(self, value):
        self.labelBlue.setText("Blue: " + str(value))
        self.blue = value

    def slotSetColor(self, value):
        self.setBackgroundColor(QColor(self.red, self.green, self.blue))
        self.labelRed.setBackgroundColor(QColor(self.red, 128, 128))
        self.labelGreen.setBackgroundColor(QColor(128, self.green, 128))
        self.labelBlue.setBackgroundColor(QColor(128, 128, self.blue))

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,
                      SIGNAL("valueChange(int)"),
                      self.slotSetColor())

which is wrong, and:

      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 doesn't differ much from connecting to a C++ signal, except that you don't have to worry so much about the type and number of arguments of the signal. To rewrite the registry.py example:

Example 7-6. Python signals and slots

#
# sigslot.py  — python signals and slots
#
from qt import *

class Button(QObject):

    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:

/home/boudewijn/doc/pyqt/ch6 $ python sigslot.py
A second function is called.
Function called

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:

Example 7-7. Python signals and slots with arguments

#
# 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"

    def printOneArgument(self, arg):
        print "One argument", arg

    def printTwoArguments(self, arg1, arg2):
        print "Two arguments", arg1, arg2

    def printVariableNumberOfArguments(self, *args):
        print "list of arguments", args

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.

It's 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:

Table 7-1. Matrix of QObject.connect() combinations.

Signal | Connected to | Syntax (Note that you can replace QObject.connect() by self.connect() everywhere.)
C++ | C++ slot of another object | QObject.connect(object1, SIGNAL("qtSignal()"), object2, SLOT("qtSlot()"))
C++ | C++ slot of ‘self' object | QObject.connect(object1, SIGNAL("qtSignal()"), SLOT("qtSlot()"))
C++ | Python slot of another object | QObject.connect(object1, SIGNAL("qtSignal()"), object2, pythonFunction)
C++ | Python slot of ‘self' object | QObject.connect(object1, SIGNAL("qtSignal()"), self.pythonFunction)
C++ | C++ signal of another object | QObject.connect(object1, SIGNAL("qtSignal()"), object2, SIGNAL("qtSignal()")
C++ | C++ signal of ‘self' object | QObject.connect(object1, SIGNAL("qtSignal()"), self, SIGNAL("qtSignal()")
Python | C++ slot of another object | QObject.connect(object1, PYSIGNAL("pySignal()"), object2, SLOT("qtSlot()"))
Python | C++ slot of ‘self' object | QObject.connect(object1, PYSIGNAL("pySignal()"), SLOT("qtSlot()"))
Python | Python slot of another object | QObject.connect(object1, PYSIGNAL("pySignal()"), object2.pythonFunction))
Python | Python slot of ‘self' object | QObject.connect(object1, PYTHON("pySignal()"), self.pythonFunction))
Python | C++ signal of another object | QObject.connect(object1, OYSIGNAL("pySignal()"), object2, SIGNAL("qtSignal()")
Python | C++ signal of ‘self' object | QObject.connect(object1, PYSIGNAL("pySignal()"), self, SIGNAL("qtSignal()")
Python | Python signal of another object | QObject.connect(object1, PYSIGNAL("pySignal()"), object2, PYSIGNAL("pySignal()")
Python | Python signal of ‘self' object | QObject.connect(object1, PYSIGNAL("pySignal()"), PYSIGNAL("pySignal()")
Prev | Home | Next
Signals and Slots in Depth | Up | Disconnecting


Disconnecting
Prev Chapter 7. Signals and Slots in Depth Next

Disconnecting

What can be bound, can be severed, and even for signals and slots there are divorce courts. You can disconnect a signal from a slot using QObject.disconnect(). Why would you want to disconnect signals? Not preparatory to removing a connected widget, for the connections are severed automatically when the signal recipient is deleted. I've never needed disconnect() myself, but with a bit of imagination, a likely scenario can be found.

Imagine therefore that you are writing a monitoring application. There are several data sources, but you want only to look at one at a time. The data keeps flowing in from a host of objects representing the sources. This is a scenario well worth writing a small test for...

First, we design the interface using BlackAdder's designer module or Qt Designer. This is a simple affair, with a combobox that contains the datasources, a read-only multi-line edit control that will show the output of the selected datasource, and a close button. The dialog window will be the main window, too.

Designing the interface

Then, we use Designer to add an extra slot to the form, switchDataSource, 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:

Connecting the activated(const QString&) signal to the switchDataSource() slot.

This raises an interesting point. If the activated(const QString&) signal passes a QString to the slot, shouldn't we define the slot switchDataSource() in the Designer as having an argument?

The answer is no— we will subclass the generated python code, and in the subclass we will override the generated slot with a function that has the requisite number of arguments. Python does not know the concept of overloading, so all functions with the same name are the same function. It is actually impossible to define the number of arguments a slot has in the Designer— you can only match signals to slots without arguments.

Having designed the form, we can generate it with a single menu-choice and start subclassing it, adding all kinds of interesting bits. First, we create the actual datasources.

Example 7-8. datasource.py — connecting and disconnecting signals and slots

#
# datasource.py — a monitor for different datasources
#

import sys, whrandom                                         (1)
from time import *                                           (2)
from qt import *

from frmdatasource import frmDataSource                      (3)
The sys module is needed for QApplication; whrandom is one of the two random modules Python provides.
The time module provides lots of time related functions.
This is the form we designed and generated with BlackAdder.
COOKIES=["""That is for every schoolboy and schoolgirl for the next  (1)
four hundred years. Have you any idea how much suffering you are going
to cause. Hours spent at school desks trying to find one joke in A
Midsummer Night's Dream? Years wearing stupid tights in school plays
and saying things like 'What ho, my lord' and 'Oh, look, here comes
Othello, talking total crap as usual' Oh, and that is Ken Branagh's
endless uncut four-hour version of Hamlet.
"", """I've got a cunning plan...""","""A Merry Messy Christmas"? All
right, but the main thing is that it should be messy -- messy cake;
soggy pudding; great big wet kisses under the mistletoe...
 """]

def randomFunction():                                        (2)
    return str(whrandom.randrange(0, 100))                   (3)

def timeFunction():                                          (4)
    return ctime(time())                                     (5)

def cookieFunction():                                        (6)
    return COOKIES[whrandom.randrange(0, len(COOKIES))]      (7)
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, there's 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, it's 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):                                   (1)

    def __init__(self, dataFunction, *args):                 (2)
        apply(QObject.__init__, (self,) + args)
        self.timer = self.startTimer(1000)                   (3)
        self.dataFunction = dataFunction                     (4)

    def timerEvent(self, ev):                                (5)
        self.emit(PYSIGNAL("timeSignal"), (self.dataFunction(),))  (6)
The DataSource class is a generic datasource. We base it on QObject so we can emit signals from it.
The constructor of DataSource takes a function as the first parameter. This is the actual dataproducing function. We saw their definitions above. Remember, every function is an object in its own right — you can pass them on as arguments, add them to object dictionaries, etc.
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):                             (1)

    def __init__(self, *args):
        apply(frmDataSource.__init__, (self,) + args)

        self.sources = {                                     (2)
            "random" : DataSource(randomFunction),
            "time" : DataSource(timeFunction),
            "cookies" : DataSource(cookieFunction)
            }

        self.cmbSource.insertStrList(self.sources.keys())    (3)
        self.currentSource=self.sources.keys()[0]            (4)
        self.connect(self.sources[self.currentSource],       (5)
                     PYSIGNAL("timeSignal"),
                     self.appendData)

    def switchDataSource(self, source):                      (6)
        source=str(source)                                   (7)
        self.disconnect(self.sources[self.currentSource],    (8)
                     PYSIGNAL("timeSignal"),
                     self.appendData)
        self.connect(self.sources[source],                   (9)
                     PYSIGNAL("timeSignal"),
                     self.appendData)
        self.currentSource=source

    def appendData(self, value):                             (10)
        self.mleWindow.insertLine(value)
        self.mleWindow.setCursorPosition(self.mleWindow.numLines(), 0)
The DataWindow class is a subclass of the generated form — class frmDataSource.
We create a Python dictionary, which takes DataSource objects (each instantiated with a different data generating function) and maps them to distinct names.
The self.cmbSource combobox is defined in the generated form. We fill the combobox with the set of keys to the dictionary. To do this, we use InsertStrList and not InsertStringList. A list of Python strings is converted automatically to a QStrList, while a QStringList object must be constructed separately.
self.currentSource is a local variable where we keep track of what datasource we're looking at.
Simply connect the "timeSignal" Python signal from one of the objects in the dictionary of datasources to the slot that will display the output.
The switchDataSource function is where interesting things happen. This function is a slot that is called whenever the user selects something from the combobox. The clicked() signal of the combobox was connected to the switchDataSource slot of the Designer.
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.
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.

Prev | Home | Next
Connecting with signals and slots | Up | A parser-formatter using signals and slots


A parser-formatter using signals and slots
Prev Chapter 7. Signals and Slots in Depth Next

A parser-formatter using signals and slots

The use of signals and slots in the previous section was an example of using signals and slots in GUI building. Of course, you can use signals and slots to link GUI 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. It's also far more ‘Pythonic' in feel than the Sax module, which is based on the way Java does things.

We'll 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.

Example 7-9. An XML parser with signals and slots

#
# qtparser.py — a simple parser that, using xmllib,
# generates a signal for every parsed XML document.
#

import sys
import xmllib                                                (1)
from qt import *

TRUE=1                                                       (2)
FALSE=0
We import the deprecated xmllib module. It is deprecated because the sax module, which uses the expat library, is a lot faster. The xmllib module is far easier to use, however, and since it uses regular expressions for its parsing, it is available everywhere, while the expat library must be compiled separately.
It is often convenient to define constants for the boolean values true and false.
class Parser(xmllib.XMLParser):                              (1)

    def __init__(self, qObject,  *args):                     (2)
        xmllib.XMLParser.__init__(self)
        self.qObject=qObject

    def start(self, document):                               (3)
        xmllib.XMLParser.feed(self, document)
        xmllib.XMLParser.close(self)
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 don't 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 isn't 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 computer's memory. You should call close() after the last bit of text has been passed to the parser.
    #
    # Data handling functions                                (1)
    #
    def handle_xml(self, encoding, standalone):              (2)
        self.qObject.emit(PYSIGNAL("sigXML"),
                          (encoding, standalone))
                                                             (3)
    def handle_doctype(self, tag, pubid, syslit, data):
        self.qObject.emit(PYSIGNAL("sigDocType"),
                         (tag, pubid, syslit, data,))        (4)

    def handle_data(self, data):
        self.qObject.emit(PYSIGNAL("sigData"),(data,))       (5)

    def handle_charref(self, ref):
        self.qObject.emit(PYSIGNAL("sigCharref"),(ref,))     (6)

    def handle_comment(self, comment):
        self.qObject.emit(PYSIGNAL("sigComment"),(comment,))  (7)

    def handle_cdata(self, data):
        self.qObject.emit(PYSIGNAL("sigCData"),(data,))      (8)

    def handle_proc(self, data):
        self.qObject.emit(PYSIGNAL("sigProcessingInstruction"),  (9)
                         (data,))

    def handle_special(self, data):                          (10)
        self.qObject.emit(PYSIGNAL("sigSpecial"), (data,))

    def syntax_error(self, message):                       (11)
        self.qObject.emit(PYSIGNAL("sigError"),(message,))

    def unknown_starttag(self, tag, attributes):           (12)
        self.qObject.emit(PYSIGNAL("sigStartTag"),
                         (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,))

    def unknown_entityref(self, ref):
        self.qObject.emit(PYSIGNAL("sigEntityRef"),(ref,))
The xmllib.XMLParser class defines a number of methods that should be overridden if you want special behavior. Even though we will only use the methods that are called when a document is started and when a simple element is opened and closed, I've implemented all possible functions here.
Every valid XML document should start with a magic text that declares itself to be XML — note that that the .ui Designer files don't comply with this requirement. This method is fired (and thus the signal is fired) when the parser encounters this declaration. Normally, it looks like this: <?xml version="1.0" standalone="no"?>, with the minor variation that standalone can also have the value "yes".
If an XML document has a documenttype, this method is called. A doctype declaration looks like this:
<!DOCTYPE book PUBLIC "-//Norman Walsh//DTD DocBk XML V3.1.4//EN"
     "http://nwalsh.com/docbook/xml/3.1.4/db3xml.dtd">

and points to a DTD — a description of what's allowed in this particular kind of XML document.

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. Python's 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.
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.
(13)
Whenever the parser encounters an unresolvable numeric character reference, this function is called.
(14)
Unknown entities are forbidden in XML — if you use an entity somewhere in your document (which you can do by placing the name of the entity between an ampersand and a semicolon), then it must be declared. However, you might want to catch occurrences of unknown entities and do something special. That's why the function unknown_entityref is implemented here. By default unknown_entityref calls the syntax_error() function of xmllib.XMLParser.

The TreeView class will show the contents of the XML file.

class TreeView(QListView):                                   (1)

    def __init__(self, *args):
        apply(QListView.__init__,(self, ) + args)
        self.stack=[]                                        (2)
        self.setRootIsDecorated(TRUE)                        (3)
        self.addColumn("Element")                            (4)

    def startDocument(self, tag, pubid, syslit, data):       (5)
        i=QListViewItem(self)
        if tag == None: tag = "None"
        i.setText(0, tag)
        self.stack.append(i)

    def startElement(self, tag, attributes):                 (6)
        if tag == None: tag = "None"
        i=QListViewItem(self.stack[-1])
        i.setText(0, tag)
        self.stack.append(i)

    def endElement(self, tag):                               (7)
        del(self.stack[-1])
The TreeView class is a simple subclass of PyQt's versatile QListView class.
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 it's 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()                                          (1)
        p=Parser(o)                                          (2)
        QObject.connect(o, PYSIGNAL("sigDocType"),           (3)
                           w.startDocument)
        QObject.connect(o, PYSIGNAL("sigStartTag"),
                           w.startElement)
        QObject.connect(o, PYSIGNAL("sigEndTag"),
                           w.endElement)

        s=open(args[1]).read()                               (4)
        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 don't 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 gui's — but it's 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.

The result of parsing a Designer .ui file.

On a final note, there is one bug in this code... See if you can find it, or consult the Section called QListView and QListViewItem in Chapter 10 for an explanation.

Prev | Home | Next
Disconnecting | Up | Conclusion


Conclusion
Prev Chapter 7. Signals and Slots in Depth Next

Conclusion

We have seen the use of signals and slots in GUI's 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.

Prev | Home | Next
A parser-formatter using signals and slots | Up | String Objects in Python and Qt


String conversions
Prev Chapter 8. String Objects in Python and Qt Next

String conversions

I will return to QCString later, as QString is used everywhere in Qt. All user-visible text is set using QStrings, and if a widget returns some text, it will return a QString as well. It should become clear that the only way to work comfortably with strings in Python and Qt is to have some automatic conversion between Python string objects and QStrings.

The conversion from a Python string to a QString is completely transparent. Any Qt class method that asks for a QString as an argument accepts any Python string (or Python Unicode string). The other way around isn't all that transparent, unfortunately. When you ask, for example, a QMultiLineEdit widget for its contents, you get a QString. If you try to use Python's regular expression engine on this object, of if you try to write it to a file, you will be surprised at the results:

Example 8-1. qstring1.py — conversion from QString to a Python string.

#
# qstring1.py - saving a QString to a file
#

from qt import *

# Construct a Python string

pyString = """Now is the summer of our sweet content,
Made o'er-cast winter by these Tudor clouds.
And I that am not shaped for black-faced war,
"""

# Construct a Qt QString

qtString=QString("""I that am rudely cast and want true majesty,
Am forced to fight,
To set sweet England free.
I pray to Heaven we fare well,
And all who fight us go to Hell.
""")

f=open("richard", "w+")
f.write(pyString)
f.flush()
f.write(qtString)
f.close()

If you run this script, you'll get the following output:

boud@calcifer:~/doc/opendoc/ch4 > python qstring1.py
Traceback (most recent call last):
  File "qstring1.py", line 26, in ?
    f.write(qtString)
TypeError: read-only character buffer, instance
boud@calcifer:~/doc/opendoc/ch4 >

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, you'll 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:

Example 8-2. qstring2.py - second try of saving a QString to a file

#
# qstring2.py - saving a QString to a file
#

from qt import *

# Construct a Python string

pyString = """Now is the summer of our sweet content,
Made o'er-cast winter by these Tudor clouds.
And I that am not shaped for black-faced war,
"""

# Construct a Qt QString

qtString=QString("""I that am rudely cast and want true majesty,
Am forced to fight,
To set sweet England free.
I pray to Heaven we fare well,
And all who fight us go to Hell.
""")

f=open("richard", "w+")
f.write(pyString)
f.flush()
f.write(str(qtString))
f.close()

I don't 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 don't have to do anything.

Prev | Home | Next
String Objects in Python and Qt | Up | QCString — simple strings in PyQt


QCString — simple strings in PyQt
Prev Chapter 8. String Objects in Python and Qt Next

QCString — simple strings in PyQt

Both Python and Qt have two types of strings: simple strings, which are sequences of bytes where every byte represents one character, and complex string objects, 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.

As an abstraction of the standard C++ null-terminated string, QCString cannot contain any null bytes ('\0'). In this respect, QCString differs from the simple Python string object. The simple Python string is often used as a container for binary data, and the string object doesn't care whether it contains null bytes. Feeding a Python string that contains null bytes to a QCString provides interesting results:

Example 8-3. empty.py - feeding zero bytes to a QCString

#
# 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()

Running the previous example produces the following output:

boudewijn@maldar:~/doc/opendoc/ch4 > python empty.py
Python string: abcdef
Length: 7
QCString: abc
Length: 3

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 Python's 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 wouldn't 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

# this string is empty
emptypystring=""

# this string contains one byte, zero
nullpystring="\0"

# this string is empty: it contains the empty string, terminated with \0
emptyqcstring=QCString("")

# this string is null: it doesn't contain data
nullqcstring=QCString()

def assertTrue(assertion, message):
    try:
        assert(assertion)
        print message, "TRUE"
    except AssertionError:
        print message, "FALSE"

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 containing 0 byte equals str(empty QCSTRING)")
assertTrue(nullqcstring is None,
    "Null QCString equals None object")

Running this gives the following output:

boudewijn@maldar:~/doc/opendoc/ch4 > python null.py
Empty Python string equals empty QCString FALSE
Empty Python string equals str(empty QCString) TRUE
Empty python string equals str(null QCString) TRUE
Python string containing 0 byte equals empty QCString FALSE
Python string containing 0 byte equals str(empty QCSTRING) FALSE
Null QCString equals None object FALSE

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 isn't a null-terminated string class:

Example 8-5. emptyqstring.py - feeding zero bytes to a QString

#
# 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()

Look at the output:

boudewijn@maldar:~/doc/opendoc/ch4 > python emptyqstring.py
Python string: abcdef
Length: 7
QString: abc
Length: 3

The unavoidable conclusion is that you shouldn't try to use Python strings as containers for binary data and then convert them to Qt string objects. Of course, there's a solution: you can use QByteArray to store binary data.

Prev | Home | Next
String conversions | Up | Unicode strings


Unicode strings
Prev Chapter 8. String Objects in Python and Qt Next

Unicode strings

Introduction to Unicode

All text that is handled by computers must be encoded. Every letter in a text has to be represented by a numeric value. For a long time, it was assumed that 7 bits would provide enough values to encode all necessary letters; this was the basis for the ASCII character set. However, with the spread of computers all over the world, it became clear that this was not enough. A whole host of different encodings were designed, varying from the obscure (TISCII) to the pervasive (latin-1). Of course, this leads to problems when you are trying to exchange texts. A western-european latin-1 user cannot easily read a Russian koi-8 text on his system. Another problem is that those small, one-byte, eight-bit character sets don't have room for useful stuff, such as extensive mathematical symbols. The solution has been to create a monster character set consisting of at least 65000 code-points including every possible character someone might want to use. This is ISO/IED-10646. The Unicode standard (http://www.unicode.org) is the official implementation of ISO/IED-10646.

Unicode is an essential feature of any modern application. Unicode is mandatory for every e-mail client, for instance, but also for all XML processing, web browsers, many modern programming languages, all Windows applications (such as Word), and KDE 2.0 translation files.

Unicode is not perfect, though. Some programmers, such as Jamie Zawinski of XEmacs and Netscape fame, lament the extra bytes that Unicode needs — two bytes for every character instead of one. Japanese experts oppose the unification of Chinese characters and Japanese characters. Japanese characters are derived from Chinese characters, historically, and even their modern meaning is often identical, but there are some slight visual differences. These complainers are often very vociferous, but Unicode is the best solution we have for representing the wide variety of scripts humanity has invented.

There are a few other practical problems concerning Unicode. Since the character set is so very large, there are no fonts that include all characters. The best font available is Microsoft's Arial Unicode, which can be downloaded for free. The Unicode character set also includes interesting scripts such as Devanagari, a script where single letters combine to from complicated ligatures. The total number of 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, it's 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.

I'll first describe the pitfalls of working with Unicode from Python, and then bring in the Qt complications.

Python and Unicode

Python actually makes a difference between Unicode strings and 'normal' strings — that is, strings where every byte represents one character. Plain Python strings are often used as character arrays representing immutable binary data. In fact, plain strings are semantically very similar to Java's byte array, or Qt's QByteArray class — they represent a simple sequence of bytes, where every byte may represent a character, but could also represent something quite different, not a human readable text at all.

Creating a Unicode string is a bootstrapping problem. Whether you use BlackAdder's Scintilla editor or another editor, it will probably not support Unicode input, so you cannot type Chinese characters directly. However, there are clever ways around this problem: you can either type hex codes, or construct your strings from other sources. In the third part of this book we will create a small but fully functional Unicode editor.

String literals

You can create a Unicode string literal by prefixing the string with the letter u, or convert a plain string to Unicode with the unicode keyword. You cannot, however, write Python code using anything but ASCII. If you look at the following script, you will notice that there is a function defined in Chinese characters (yin4shua1 means print), that tries to print the opening words of the Nala —, a Sanskrit epos. Python cannot handle this, so all actual code must be in ASCII.

A Python script written in Unicode.

Of course, it would be nice if we could at least type the strings directly in UTF-8, as shown in the next screenshot:

A Python script with the strings written in Unicode.

Unfortunately, this won't 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 don't talk Unicode, such as print. By default this is 7-bits ASCII. Running the script gives the following error:

boudewijn@maldar:~/doc/opendoc/ch4 > python unicode2.py

Traceback (most recent call last):
  File "unicode2.py", line 4, in ?
    nala()
  File "unicode2.py", line 2, in nala
    print u"à¤à¤¸à¥à¤¦ à¤°à¤¾à¤à¤¾ à¤¨à¤²à¥ à¤¨à¤¾à¤® "
UnicodeError: ASCII encoding error: ordinal not in range(128)

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:

Explicitly telling Python that a string literal is in the utf-8 encoding.

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!

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.

boudewijn@maldar:/usr/local/lib/python2.0/encodings > ls *py
__init__.py cp1254.py cp852.py cp869.py      iso8859_5.py
aliases.py  cp1255.py cp855.py cp874.py      iso8859_6.py
ascii.py    cp1256.py cp856.py cp875.py      iso8859_7.py
charmap.py  cp1257.py cp857.py iso8859_1.py  iso8859_8.py
cp037.py    cp1258.py cp860.py iso8859_10.py iso8859_9.py
cp1006.py   cp424.py  cp861.py iso8859_13.py koi8_r.py
cp1026.py   cp437.py  cp862.py iso8859_14.py latin_1.py
cp1250.py   cp500.py  cp863.py iso8859_15.py mac_cyrillic.py
cp1251.py   cp737.py  cp864.py iso8859_2.py  mac_greek.py
cp1252.py   cp775.py  cp865.py iso8859_3.py  mac_iceland.py
cp1253.py   cp850.py  cp866.py iso8859_4.py  mac_latin2.py

Reading from files

The same problem will occur when reading text from a file. Python has to be explicitly told when the file is in an encoding different from the default encoding. Python's file object reads files as bytes and returns a plain string. If the contents are not encoded in Python's default encoding (ASCII), you will have to be explicit about it. Let's try reading the preceding script, unicode3.py, which was saved in utf-8 format.

Example 8-6. Loading an utf-8 encoded text

#
# readutf8.py - read an utf-8 file into a Python Unicode string
#

import sys, codecs

def usage():
    print """
Usage:

python readutf8.py file1 file2 ... filen
"""

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 option allows you to specify the encoding when opening the file, instead of only when writing to the file.

Other ways of getting Unicode characters into Python string objects

We've now seen how to get Unicode data in our strings from either literal text entered in the Python code or from files. There are several other ways of constructing Unicode strings. You can build strings using the Unicode escape codes, or from a sequence of Unicode characters.

For this purpose, Python offers unichr, which returns a Unicode string of exactly one character wide, when called with a numerical argument between 0 and 65535. This can be useful when building tables. The resultant character can, of course, only be printed when encoded with the right encoding.

Example 8-7. Building a string from single Unicode characters

#
# 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))

# Combine the characters into a string - this is
# faster than doing u=u+uniChr(c) in the loop
u=u"" + string.join(uList,"")

f=codecs.open("cyrillic1.ut8", "aw+", "utf-8")
f.write(u)
f.flush()

f=open("cyrillic2.ut8", "aw+")
f.write(u.encode("utf-8"))
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:

Python 2.1 (#1, Apr 17 2001, 20:50:35)
[GCC 2.95.2 19991024 (release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> u=u"\u0411\u0412

About codecs and locales: With all this messing about with codecs you will no doubt have wondered why Python can't 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:

# Set the string encoding used by the Unicode implementation.  The
# default is 'ascii', but if you're willing to experiment, you can
# change this.

encoding = "ascii" # Default value set by _PyUnicode_Init()

if 0:
    # Enable to support locale aware default string encodings.
    import locale
    loc = locale.getdefaultlocale()
    if loc[1]:
        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 don't 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, it's 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 don't need to set a default encoding per application, because the system will take care of that, but I'd still like the power.

Fortunately, there's a solution. I'll probably get drummed out of the regiment for suggesting it, but it's so useful, I'll tell it anyway. Create a file called sitecustomize.py as follows:

Example 8-8. sitecustomize.py — saving a useful function from wanton destruction

#
# 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 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:

Example 8-9. uniqstring3.py - messing with Unicode strings using utf-8 as default encoding

#
# uniqstring3.py - coercing Python strings into and from QStrings
#
from qt import QString
import sys

sys.setappdefaultencoding("utf-8")

s="A string that contains just ASCII characters"
u=u"\u0411\u0412 - a string with a few Cyrillic characters"

print s
print u

Qt and Unicode

As mentioned earlier, QString is the equivalent of a Python Unicode string. You can coerce any Python string or any Python Unicode object into a QString, and vice versa: you can convert a QString to either a Python string object, or to a Python Unicode object.

If you want to create a plain Python string from a QString object, you can simply apply the str() function to it: this is done automatically when you print a QString.

Unfortunately, there's a snake in the grass. If the QString contains characters outside the ASCII range, you will hit the limits dictated by the default ASCII codec defined in Python's site.py.

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

s="A string that contains just ASCII characters"
u=u"\u0411\u0412 - a string with a few Cyrillic characters"

qs=QString(s)
qu=QString(u)

print str(qs)
print str(qu)

boud@calcifer:~/doc/opendoc/ch4 > python uniqstring1.py
A string that contains just ASCII characters

Traceback (most recent call last):
  File "uniqstring1.py", line 13, in ?
    print qu
  File "/usr/local/lib/python2.1/site-packages/qt.py", line 954, in __str__
    return str(self.sipThis)
UnicodeError: ASCII encoding error: ordinal not in range(128)

If there's 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.

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

s="A string that contains just ASCII characters"
u=u"\u0411\u0412 - a string with a few Cyrillic characters"

qs=QString(s)
qu=QString(u)

print unicode(qs)
print unicode(qu)

Prev | Home | Next
QCString — simple strings in PyQt | Up | Python Objects and Qt Objects


Circular references
Prev Chapter 9. Python Objects and Qt Objects Next

Circular references

You can cause a memory leak in Python by creating circular references, or by never deleting the last reference to an object. The latest versions of Python support a garbage collector that can detect circular references and clean them up. It's quite difficult to create circular references, but it's very easy to accidentally keep references lying about. The next snippet shows the circularity of referring from A to B to A...

Example 9-2. circular.py - circululululular references

#
# 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

boudewijn@maldar:~/doc/opendoc/ch3 > python circular.py
<__main__.A instance at 0x8199bb4>
<__main__.B instance at 0x8199c04>
<__main__.A instance at 0x8199bb4>
<__main__.A instance at 0x8199bb4>

If you delete the instance a, you only make the objects inaccessible; because b still refers to a, there's a reference for the reference counter, and a will not be destroyed. 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.)

Prev | Home | Next
Python Objects and Qt Objects | Up | Qt objects, Python objects and shadow objects


Qt objects, Python objects and shadow objects
Prev Chapter 9. Python Objects and Qt Objects Next

Qt objects, Python objects and shadow objects

What happens when you create a Qt object from Python? A Qt object is an instance of a C++ class, but in order to be able to manipulate that object from Python, there needs to be a Python object, too. The answer is that both are created: a C++ object that contains the real functionality, and a Python object that "wraps" the C++ object.

Thus, when you call QWidget() from Python, three things are created:

  • The C++ QWidget instance. Actually it will be a sipQWidget instance which is a sub-class of QWidget - it is needed to be a catcher for QWidget's virtual methods and to expose its protected methods and enums.

  • the Python object shadow/proxy which is a thin wrapper around the C++ instance and which has a unique Python type.

  • the Python class instance that you actually deal with in your script. Its instance dictionary contains a reference (named __sipThis__) to the shadow object.

The two Python objects are needed because the programmer wants a class instance, but a Python class instance doesn't provide you with the capability of wrapping a C++ pointer.

In fact, both the Qt library and the PyQt library can create C++ objects. These are passed between them. For instance, an object could be created by PyQt and passed to Qt, or it could be created by Qt and passed to Python. Both Qt and Python have the concept of deleting things — so there has to be a method of making sure that C++ instances are deleted properly. If both try to delete the same instance your program crashes, if neither do then you get memory leaks. Therefore there is the concept of ownership: the current owner (PyQt or Qt) is responsible for deleting the C++ instance. Ownership of an instance may be transferred between PyQt and C++ during the life of the instance.

Mostly you won't 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.)

Prev | Home | Next
Circular references | Up | References and ownership


References and ownership
Prev Chapter 9. Python Objects and Qt Objects Next

References and ownership

Let's investigate the actual creation and deletion of object - both Python and Qt have a role to play here - a role they mostly perform without surprising the programmer. Still, there are circumstances you should be aware of.

Example 9-3. qtrefs1.py — about Qt reference counting

#
# qtrefs1.py
#

import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        topbutton=QPushButton("A swiftly disappearing button", None)
        topbutton.show()

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)

Here, we create a window, and in the constructor (the __init__ method), we create a QPushButton. That button really should appear as a second toplevel window - but it doesn't. The reason is that the only reference to the object is the variable topbutton, and that variable goes out of scope once the constructor method finishes. The reference ceases to exist, and so the object is deleted.

If we want to keep the button alive, we should keep the reference alive. The easiest way to do that is to associate the button more closely with the containing window object. It is customary to refer to the containing object with the variable self. Python passes a reference to an object as the first argument to any instance method. This reference is usually named self.

So, if we adapt the preceding example as follows, we keep the object:

Example 9-4. qtrefs2.py - keeping a Qt widget alive

#
# qtrefs2.py
#

import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        self.topbutton=QPushButton("A nice and steady button",
                                   None)
        self.topbutton.show()

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)

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 widget's 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:

Example 9-5. qtrefs3.py - Qt parents and children

#
# qtrefs3.py
#

import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        parentedButton=QPushButton("A nice and steady button "
                                  + "that knows its place",
                                  self)
        parentedButton.resize(parentedButton.sizeHint())

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.

Example 9-6. Eradicating a widget

#
# qtrefs4.py - removing a widget
#

import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        self.parentedButton=QPushButton("A nice and steady button "
                                   + "that knows its place",
                                   self)
        self.parentedButton.resize(self.parentedButton.sizeHint())
        self.connect(self.parentedButton,
                     SIGNAL("clicked()"),
                     self.removeButton)

    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).

Example 9-7. children.py - getting the children from a single parent

#
# children.py
#

import sys
from qt import *

def printChildren(obj, indent):
    children=obj.children()
    if children==None:
        return
    for child in children:
        print indent, child.name(), child.__class__
        printChildren(child, indent + "  ")

class PyPushButton(QPushButton): pass

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        mainwidget=QWidget(self, "mainwidget")
        layout=QVBoxLayout(mainwidget, 2, 2, "layout")
        button1=QPushButton("button1", mainwidget, "button1")
        button2=PyPushButton("button2", mainwidget, "button2")
        layout.addWidget(button1)
        layout.addWidget(button2)

        self.setCentralWidget(mainwidget)
        printChildren(self, "  ")

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 children.py will give the following output:

boudewijn@maldar:~/doc/opendoc > python children.py
   hide-dock qt.QObject
   mainwidget qt.QWidget
     layout qt.QVBoxLayout
     button1 qt.QPushButton
     button2 __main__.PyPushButton
   unnamed qt.QObject
     unnamed qt.QObject
     unnamed qt.QObject
       unnamed qt.QObject
       unnamed qt.QObject
     unnamed qt.QObject

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.

Example 9-8. Iterating over children

#
# children.py
#

import sys
from qt import *

def printChildren(obj, indent):
    iter = obj.iterator()
    while iter.current():
        print "current:", iter.current()
        print "next:", iter.next()

class PyPushButton(QPushButton): pass

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        mainwidget=QWidget(self, "mainwidget")
        layout=QVBoxLayout(mainwidget, 2, 2, "layout")
        button1=QPushButton("button1", mainwidget, "button1")
        button2=PyPushButton("button2", mainwidget, "button2")
        button3=PyPushButton("button3", mainwidget, "button3")
        layout.addWidget(button1)
        layout.addWidget(button2)
        layout.addWidget(button3)

        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)

boud@calcifer:~/doc/pyqt/src/qt2/ch3 > python layoutchildren.py
current: <qt.QLayoutItem instance at 0x82ba8b4>
next: <qt.QLayoutItem instance at 0x82ba9dc>
current: <qt.QLayoutItem instance at 0x82ba9dc>
next: <qt.QLayoutItem instance at 0x82baa8c>
current: <qt.QLayoutItem instance at 0x82baa8c>
next: None

Finally, let's 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 doesn't get deleted, even when the Python reference goes out of scope.

>>> from qt import QObject
>>> class A:
...     def __init__(self):
...             self.o=QObject()
...             child = QObject(self.o)
...
>>> a=A()
>>> print a
<__main__.A instance at 0x821cdac>
>>> print a.o
<qt.QObject instance at 0x821ce04>
>>> print a.o.children()
[<qt.QObject instance at 0x821cf54>]
>>>

On the other hand, the following won't work, because as soon as the execution flow leaves the constructor, o is garbage collected, and child, is then garbage-collected, too, since it isn't owned by a Qt object, and Python doesn't 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

On the other hand, it isn't 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 isn't 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?:

>>> class D(QObject):
...        def __init__(self):
...             QObject.__init__(self)
...             o=QObject(self)
...             child = QObject(o)
...
>>> d=D()
>>> d.children()
[<qt.QObject instance at 0x821d614>]
>>> d.children()[0].children()
[<qt.QObject instance at 0x821d7c4>]
>>>

As you can see, o doesn't 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.

Prev | Home | Next
Qt objects, Python objects and shadow objects | Up | Other C++ objects


Other C++ objects
Prev Chapter 9. Python Objects and Qt Objects Next

Other C++ objects

There are many kinds of objects that do not fit in a parent-child relationship, because they are not derived from QObject, such as QFont or QColor. You must keep references to these objects yourself for as long as you need them, or you will lose the objects. (This doesn't differ from normal Python objects, of course — it will only be surprising if you come from C++ programming to Python.)

Prev | Home | Next
References and ownership | Up | Connecting signals and slots


Connecting signals and slots
Prev Chapter 9. Python Objects and Qt Objects Next

Connecting signals and slots

If you have worked with pointers in C++ or object references in other languages, then you will probably have been wondering whether creating signal/slot connections also means creating object references. Remember, object A can register interest in some signals Object B emits.

This means that somewhere there must be code that object B calls when it wants to emit that signal; and that there must be code that is called to notify object A. In order for that to be possible, a reference must be stored to object A. This is known as the observer pattern:

Example 9-9. sigslot.py - a simple signals/slots implementation in Python, following the Observer pattern

#
# sigslot.py - a simple signals/slots implementation in Python
#

class ClassA:

    def __init__(self):
        self.interestedObjects=[]

    def connect(self, obj):
        self.interestedObjects.append(obj)

    def sendSignal(self):
        for obj in self.interestedObjects:
            obj.slot("This is a signal from ClassA")

class ClassB:

    def slot(self, message):
        print "Object with ID", id(self), "Got signal: message"

objectA=ClassA()
objectB=ClassB()
objectA.connect(objectB)
objectC=ClassB()
objectA.connect(objectC)
objectD=ClassB()
objectA.connect(objectD)

objectA.sendSignal()

In this exceedingly simplified implementation of the signals and slots concept, objectA actually stores the references to interested objects. If the PyQt signals and slots mechanism were implemented like this, objects would not be deleted unless the all objects they had connections to were deleted as well. This puts a burden on 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 don't 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.

Prev | Home | Next
Other C++ objects | Up | Object and class introspection


Object and class introspection
Prev Chapter 9. Python Objects and Qt Objects Next

Object and class introspection

Both Python and Qt offer a great deal of object introspection functionality — that is, methods of determining at runtime what kind of class an object is an instance of, or what methods an object implements. It has often been difficult to make Python and Qt introspection mesh well. One example is the QObject.className(), which returns the name of the class of an object. Until PyQt version 2.5, this function always returned QObject, instead of the Python class name. Since that version, however, it returns the true class name:

Example 9-10. Object introspection using Qt

Python 2.1 (#1, Apr 17 2001, 20:50:35) [GCC 2.95.2 19991024
(release)] on linux2 Type "copyright", "credits" or "license"
for more information.
>>> from qt import *
>>> t=QTimer()
>>> t.className() 'QTimer'
>>> class A(QTimer): pass ...
>>> 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:

Example 9-11. Object introspection using Python

        >>> t.__class__ <class qt.QTimer at 0x8232cc4>
        >>> a.__class__ <class __main__.A at 0x826c2ac>
        >>> a.__class__.__bases__ (<class qt.QTimer at
        0x8232cc4>,)

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.

Prev | Home | Next
Connecting signals and slots | Up | Qt Class Hierarchy


Base classes
Prev Chapter 10. Qt Class Hierarchy Next

Base classes

You won't often create objects from Qt base classes, but you might need to subclass QObject now and then. The basic classes that Qt is built on are QObject and QEvent. There are several other classes used for the construction of high-level datatypes, and a large number of other classes that support working with, for instance, fonts, images and colors.

QObject brings together support for:

  • Signals and slots

  • Timers

  • Object ownership hierarchy

  • Event handling and event filters

  • Introspection

  • Properties

Signals and slots are meant for communication between objects—for 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 doesn't 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.

Example 10-1. event1.py - handling mouse events in PyQt

#
# event1.py
#
from qt import *
import sys

class Painting(QWidget):

    def __init__(self, *args):
        apply(QWidget.__init__,(self, ) + args)

    def paintEvent(self, ev):
        self.p = QPainter()
        self.p.begin(self)
        self.p.fillRect(self.rect(), QBrush(Qt.white))
        self.p.flush()
        self.p.end()

    def mouseMoveEvent(self, ev):
        self.p = QPainter()
        self.p.begin(self)
        self.p.drawLine(self.currentPos, ev.pos())
        self.currentPos=QPoint(ev.pos())
        self.p.flush()
        self.p.end()

    def mousePressEvent(self, ev):
        self.p = QPainter()
        self.p.begin(self)
        self.p.drawPoint(ev.pos())
        self.currentPos=QPoint(ev.pos())

        self.p.flush()
        self.p.end()

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.painting=Painting(self)
        self.setCentralWidget(self.painting)

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)

event1.py

In handling methods such as mousePressEvent(), It is customary to use ev as the name for the QEvent parameter. In this example, all mouse press events and mouse move events are consumed by the Painting class.

Prev | Home | Next
Qt Class Hierarchy | Up | Application classes


Application classes
Prev Chapter 10. Qt Class Hierarchy Next

Application classes

Most gui applications consist of a main window embellished with toolbars, a menu bar, and a statusbar, and have a hole in the middle. The hole in the middle can be filled with a specialized widget, or it may be a sort of desktop-in-a- desktop, with its own windows, and with sub-windows that dock themselves to the sides of the main window. Often, and application includes a few secondary windows, dialogs and a number of small popup-windows that warn or inform the user.

We have already worked with a few of these components. There is QApplication, the base of every PyQt application. QApplication wants to receive the command-line arguments to determine the look and feel of the application, as shown in the Section called As simple as they come in Chapter 6.

Then there's the main window — as shown in the Section called A better Hello World in Chapter 6, you can have an unlimited number of main windows, and not all those main windows have to be of the same class, as long as they inherit QMainWindow.

We have yet to add the frills to the main window. PyQt makes it quite easy to do this. The best way of adding menu options and toolbar buttons is to create a QAction for each action. QAction is a class that brings together user interface information about a certain action the user can undertake in your application.

For instance, if you're developing a network client application, one of the actions 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           ",
             "  XX            ",
             "  X             "
             ]

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        self.setCaption("Network Client")

        # 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 central server.")
        self.action.setStatusTip("Log in to the central 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)

        # Set a central widget
        self.editor=QMultiLineEdit(self)
        self.setCentralWidget(self.editor)

    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

When, in Chapter 24, we reach the pinnacle of development of Kalam, the extensible Unicode editor, you will have become very familiar with QAction.

Multiple document windows with QWorkspace

The MDI (multiple document interface) is a paradigm made popular by Microsoft, in which one application window contains several document windows. For certain classes of application, such as programming editors, this is a very comfortable paradigm, but most users tend to get very confused when confronted with windows that don't 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. Let's 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, you'll 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.

Example 10-3. fragment from mdi.py - ten little scribbling windows

...
class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.setCaption("MDI Scribbler")
        self.workspace=QWorkspace(self, "workspace")
        self.winlist=[]
        for i in range(10):
            win=Painting(self.workspace)
            win.resize(100,100)
            win.setCaption("Window " + str(i))
            self.winlist.append(win)
        self.setCentralWidget(self.workspace)
...

mdi.py - ten little scribbling windows.

Prev | Home | Next
Base classes | Up | Widget foundations: QWidget


Widget foundations: QWidget
Prev Chapter 10. Qt Class Hierarchy Next

Widget foundations: QWidget

All Qt widgets and all visible components are founded upon QWidget — this monster class provides all event handling, all style handling and countless other chores. To help with the handling of these tasks, there are other classes, such as QPixmap, QColor, QFont or QStyle.

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:

Example 10-4. event2.py - using QWidget to create a custom, double-buffered drawing widget.

#
# event2.py
#
from qt import *
import sys

class Painting(QWidget):

    def __init__(self, *args):
        apply(QWidget.__init__,(self, ) + args)
        self.buffer = QPixmap()

    def paintEvent(self, ev):
        # blit the pixmap
        bitBlt(self, 0, 0, self.buffer)

    def mouseMoveEvent(self, ev):
        self.p = QPainter()
        self.p.begin(self.buffer)
        self.p.drawLine(self.currentPos, ev.pos())
        self.currentPos=QPoint(ev.pos())
        self.p.flush()
        self.p.end()
        bitBlt(self, 0, 0, self.buffer)

    def mousePressEvent(self, ev):
        self.p = QPainter()
        self.p.begin(self.buffer)
        self.p.drawPoint(ev.pos())
        self.currentPos=QPoint(ev.pos())
        self.p.flush()
        self.p.end()
        bitBlt(self, 0, 0, self.buffer)

    def resizeEvent(self, ev):
        tmp = QPixmap(self.buffer.size())
        bitBlt(tmp, 0, 0, self.buffer)
        self.buffer.resize(ev.size())
        self.buffer.fill()
        bitBlt(self.buffer, 0, 0, tmp)

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.painting=Painting(self)
        self.setCentralWidget(self.painting)

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)

event2.py - persistent drawing

By drawing to QPixmap instead of to QWidget, and blitting the contents of that pixmap to the widget, the drawing will be kept. Note also how much smoother the drawing feels, despite the extra work the script has to do. This technique is called double buffering, and is the alpha and the omega of graphics programming. Of course, there's still a small problem with resizing... In fact, if you want to build your own widgets from the ground up using QWidget, you're always in for more work than you reckoned with.

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 it's quite portable. If you replace the resizeEvent() in the event2.py example with the following code, you'll see the effect:

Example 10-5. snippet from event3.py - a peach puff drawing board

...
    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(), you'd 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 you're designing complex widgets, you will want to investigate setBackgroundMode and the BackgroundMode flags.

QPixmap, QBitmap and QImage

We have already been using a QPixMap to double buffer the scribblings in the previous two examples. QPixmap is not the only image class PyQt offers: there's also QBitmap, which is just like QPixmap, but for black and white images only, and 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. There's 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 they're 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 you'd be well advised to stick to this standard.

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

  • widgets : QWidget (and all children of QWidget)

What can be drawn on one device, can be drawn on all devices, so it's uncommonly easy to print on paper what can be drawn on screen. Copying batches of pixels from one paint device to another is blindingly fast if you use the bitBlt global function, as we did above for our double-buffered graphics editor.

Note that you cannot create any paint device until you have created a QApplication. This includes QPixmaps. The following variant on action.py won't work, even though it seems a good idea to pre-create the pixmap, instead of converting the xpm data on constructing the QAction:

Example 10-6. fragment from action2.py - You cannot create a QPixmap before a QApplication

#
# 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):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
...
        self.action.setIconSet(QIconSet(connectIcon))
...

Running this gives the following result:

boudewijn@maldar:~ > python action2.py
QPaintDevice: Must construct a QApplication before a QPaintDevice
Aborted

Chapter 21 deals with painters and paintdevices in quite a lot of detail, while Chapter 24 deals with printing to paper.

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, Qt's 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 Qt's layout managers, instead of positioning widgets with pixel-precision yourself, you will have little trouble dealing with the geometry differences between Window's Arial font and KDE's 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.

QFont provides all necessary functions to select encodings (or scripts in Qt3), font families, styles and sizes. There's 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 it's 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.

Font metrics

Prev | Home | Next
Application classes | Up | Basic widgets


Basic widgets
Prev Chapter 10. Qt Class Hierarchy Next

Basic widgets

All basic screen components are available in PyQt: buttons, frames, edit controls, listboxes and comboboxes. All these widgets can be drawn in any number of styles, and you can even define your own style. Note that the window titlebar and borders are not defined by the widget style, but by the system you are running. The borders in these screenshots are from the KDE System++ style.

Basic widgets in the CDE style

Basic widgets in the motif style

Basic widgets in the motif plus style

Basic widgets in the platinum style

Basic widgets in the SGI style

Basic widgets in the Windows style

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.

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 QPushButton's central importance, I want to present a ‘Hello' application with four buttons, each in a different style. This also shows a frame.

Example 10-7. buttons.py - Four pushbuttons saying ‘hello'.

#
# buttons.py
#

from qt import *
import sys

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.setCaption("Buttons")

        self.grid=QGrid(2, self)
        self.grid.setFrameShape(QFrame.StyledPanel)

        self.bn1=QPushButton("Hello World", self.grid)
        self.bn1.setFlat(1)

        self.bn2=QPushButton("Hello World", self.grid)
        self.bn2.setDefault(1)

        self.bn3=QPushButton("Hello World", self.grid)
        self.bn3.setToggleButton(1)
        self.bn3.setDown(1)

        self.bn4=QPushButton("Hello", self.grid)

        self.setCentralWidget(self.grid)

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

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.

Example 10-8. label.py - a label associated with an edit control

#
# label.py
#
import sys
from qt import *

class dlgLabel(QDialog):

    def __init__(self,parent = None,name = None,modal = 0,fl = 0):
        QDialog.__init__(self,parent,name,modal,fl)
        self.setCaption("label dialog")
        if name == None:
            self.setName("dlgLabel")

        self.layout=QHBoxLayout(self)
        self.layout.setSpacing(6)
        self.layout.setMargin(11)

        self.label=QLabel("&Enter some text", self)
        self.edit=QLineEdit(self)
        self.label.setBuddy(self.edit)

        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

If you press alt-e after starting the label.py script (which is on the CD-ROM), you'll 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 user's 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):

    def __init__(self,parent = None,name = None,modal = 0,fl = 0):
        QDialog.__init__(self,parent,name,modal,fl)
        self.setCaption("label dialog")
        if name == None:
            self.setName("dlgLabel")

        self.layout=QHBoxLayout(self)
        self.layout.setSpacing(6)
        self.layout.setMargin(11)

        self.label=QLabel("&Enter some text", self)
        self.edit=QLineEdit(self)
        self.label.setBuddy(self.edit)

        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()
    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.

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().

Example 10-9. radio.py - a group of mutually exclusive options

#
# label.py
#
import sys
from qt import *

class dlgRadio(QDialog):

    def __init__(self,parent = None,name = None,modal = 0,fl = 0):
        QDialog.__init__(self,parent,name,modal,fl)
        self.setCaption("radiobutton dialog")
        if name == None:
            self.setName("dlgRadio")
        self.layout=QVBoxLayout(self)

        self.buttonGroup=QVButtonGroup("Choose your favourite", self)
        self.radio1=QRadioButton("&Blackadder I", self.buttonGroup)
        self.radio2=QRadioButton("B&lackadder II", self.buttonGroup)
        self.radio3=QRadioButton("Bl&ackadder III", self.buttonGroup)
        self.radio4=QRadioButton("Bla&ckadder Goes Forth", self.buttonGroup)

        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

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 there's a checked, unchecked and a doesn't apply state — the doesn't apply state is usually rendered to look just like a completely disabled checkbox. However, it is sometimes necessary to introduce this behavior. Imagine a dialog box that allows the user to set a filter:

A dialog with a tri-state checkbox in the ‘doesn't apply' state.

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.

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, doesn't 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:

Example 10-10. listbox.py - A listbox where data can be associated with an entry

#
# listbox.py
#
# listbox with key-to-index and index-to-key mapping
#
import sys
from qt import *

class AssociativeListBox(QListBox):

    def __init__(self, *args):
        apply(QListBox.__init__,(self,)+args)
        self.text2key = {}
        self.key2text = {}
        self.connect(self, SIGNAL("selected(int)"),
                     self.slotItemSelected)

    def insertItem(self, text, key):
        QListBox.insertItem(self, text)
        self.text2key [self.count() - 1] = key
        self.key2text [key]=self.count() - 1

    def currentKey(self):
        return self.text2key[self.currentItem()]

    def setCurrentItem(self, key):
        if self.key2text.has_key(key):
            QListBox.setCurrentItem(self, self.key2text[key])

    def slotItemSelected(self, index):
        key=self.currentKey()
        self.emit(PYSIGNAL("itemSelected"),
                  (key, self.currentText()) )

    def removeItem(self, index):
        del self.text2key[self.currentItem()]
        del self.key2text[index]
        QListView.removeItem(self, index)

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.listbox=AssociativeListBox(self)
        self.listbox.insertItem("Visible text 1", "key1")
        self.listbox.insertItem("Visible text 2", "key2")
        self.listbox.insertItem("Visible text 3", "key3")
        self.setCentralWidget(self.listbox)

        self.connect(self.listbox,PYSIGNAL( "itemSelected"), self.printSelection)

    def printSelection(self, key, text):
            print "Associated with", key, "is", text

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.

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.

QLineEdit

This is a simple one-line edit control, familiar to gui users everywhere. It supports copy, cut and paste and redo, too. There's a special mode for password boxes.

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 that's 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. You're supposed to use the new, advanced QTextEdit, instead. After creating a QTextEdit object, you'd 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.

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. PyQt's 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.

QProgressBar

QProgressBar gives you a horizontal progressbar — it's quite simple, even though it can be set to use one of several different styles. There's also QProgressDialog which can be used for lengthy actions that completely block access to the application. Since PyQt doesn't really support multi-threading, it's 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 it's best to place the progress bar in the statusbar of your application, instead of a separate non-modal progress dialog. the Section called A painting example in Chapter 21 gives an example of the use of a timer.

QSlider and other small fry

There are several other, simple user-interface widgets: QDial, QLCDNumber, QScrollBar, QSizeGrip, QSpinBox and QToolButton. These widgets are seldom used, mostly because they are rather overspecialized.

QDial is a potentio-meter like knob. Twiddling it demands a fair proficiency with the mouse, and the keyboard interface isn't immediately obvious. See Example 7-5 for an example of using QDial.

QLCDNumber is a kind of label which can display numbers in an lcd-like format. It's mostly interesting for historical reasons — the first version was written for the Sinclair ZX-Spectrum, a 1.9 MHz Z80 computer with a rubber keyboard and 48 Kb of ram.

QScrollBar looks to be quite useful, because, on the face of it, any gui application is full of scrollbars. But those scrollbars come automatically with the edit-controls, listboxes and other scrollable widgets, and the QScrollBar is seldom used in isolation, and then mostly for the same purpose as QDial— as a range control. If you want to use it for scrolling a section on screen, use the QScrollView class instead.

The QSizeGrip is extremely obscure, being at its peak form only in statusbars of resizable windows. And those QStatusBars can take care of their sizegrips themselves.

QSpinBox is another range control. It's often used to let the user select a size for a font — she can either type the size directly, or use the little arrows to choose a larger or smaller size. Spinboxes are often quite fiddly to use.

QToolButton is a special button that carries more often a picture than a text — it's mostly used in toolbars, where you can add buttons without explicitly creating instances of this class.

Prev | Home | Next
Widget foundations: QWidget | Up | Advanced widgets


Advanced widgets
Prev Chapter 10. Qt Class Hierarchy Next

Advanced widgets

It is with the advanced widgets that the real fun starts. PyQt has a range of really powerful widgets that allows you to build any kind of modern application you desire.

You will notice that many advanced Qt widgets are formed from the combination of a manager widget class and an item class. This holds for QCanvas with QCanvasItem, for QListView with QListViewItem and for many others.

QSimpleRichText, QTextView and QTextBrowser

These classes implement rich text viewers. They use html text and stylesheets to present data to a user. QTextView is limited to one page of text, while QTextBrowser includes hyperlink navigation. The class QStyleSheet is used to determine the graphical rendering of the contents of QTextView and QTextBrowser. QSimpleRichText is more like a label in use, and is intended for smaller texts. Indeed, if you stuff a QLabel with rich text, it will get displayed using QSimpleRichText. These classes do not provide a complete web-browser rendering engine — that would be too much for a mere toolkit, but the rendering is quite good.

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 doesn't have a notion of the concept of ‘page'. The KDE Office wordprocessor, KWord is built around it.

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!)

QListView and QListViewItem

This is possibly the most overworked PyQt class — it seems to be used in almost every application. QListView doubles as a listview and a treeview. People coming from a Visual Basic background will be delighted with the ease of use of this treeview. Adding an item in a tree is a simple matter of creating a QListViewItem with another QListViewItem for a parent.

Example 10-11. tree.py - building a tree

#
# tree.py - a simple tree with QListView
#
import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.tree = QListView(self)
        self.setCentralWidget(self.tree)
        self.tree.addColumn("item")
        self.tree.setRootIsDecorated(1)
        self.items=[]
        self.items.append(QListViewItem(self.tree, "testself1"))
        self.items.append(QListViewItem(self.items[-1], "child 1"))
        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 the Section called A parser-formatter using signals and slots in Chapter 7? 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.)

QIconView and QIconViewItem

If you are familiar with the Listview control that's available on Windows, you might be somewhat surprised to learn that Qt's listview doesn't include an icon view mode. There is a separate icon view class QIconView that provides all the functionality you might expect, except for the easy switching between listview mode and iconview mode. You will need to use two widgets in a widget-stack (QWidgetStack) for that.

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.

QCanvas, QCanvasView and QCanvasItems

This is a very powerful combination. Canvases are typically used in graphics applications, games or applications where a complex layout of text is required. QCanvasView is the real widget that includes scrollbars and can react to mouse presses or keyboard interaction. A QCanvasView can show (part of) a QCanvas, but a QCanvas can be shown on more than one QCanvasView. You can place QCanvasItems on a QCanvas. These items can represent simple geometric forms, chunks of text or sprites (sprites are independently moving, animated pictures). The following classes implement QCanvasItem:

  • 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.

the Section called QCanvas in Chapter 21 shows a practical use for a QCanvas.

QTable, QTableItem and QTableView (or QGridView)

QTable and QTableView are completely different classes, but they should be discussed together, since both implement a way for a developer to present tabular data.

QTableView is rather difficult to use — it has a primitive, low-level interface and code based on it tends to be buggy. There is a lot of flexibility built into QTableView, but you cannot add widgets to cells. It has been deprecated in Qt3, where you can use the QGridView class instead.

QTable, by contrast, is a very high-level spreadsheet-like control, eminently suited for the presentation of database data. QTableItems are the items that fill the cells of a QTable, and are editable. It's easy to have a combobox or line editor pop-up when the user selects a certain cell. Windows users especially will know about the vast amount of ‘grid controls' that can be bought for Visual Basic or Visual C++ — QTable is the Qt equivalent, only not so bloated as most of those grids are.

Prev | Home | Next
Basic widgets | Up | Layout managers


Layout managers
Prev Chapter 10. Qt Class Hierarchy Next

Layout managers

One of the great strengths of PyQt is the use of layout managers. Formerly, gui designers had to position and size every element in their dialogs with pixel precision. Of course, this meant that enlarging a window wouldn't show the user more data, just a vast desert of boring grey pixels. Worse, when making a window smaller, data would be obscured. Even worse, there are still applications being made where you cannot resize the windows at all.

Too large...

Too small.

It's 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 Qt's 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 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.

Note: QMainWindow provides its own layout management— it manages the size and position of the menubar, toolbar or toolbars, statusbar and the widget in the middle. If that widget is not composed of several widgets, the management will be quite sufficient. If there are several widgets constrained by a QSplitter, the management will likewise be sufficient, because in that case, the QSplitter will be the central widget. If you have a more complex assembly of widgets, you will have to create a dummy central QWidget that contains a layoutmanager that manages those widgets in a pleasing way. You can also directly add a layout manager to QMainWindow, but PyQt will natter about a layout manager being added to a widget that already had one. It's not dangerous, though. See Example 10-12 for an example of such a dummy central widget.

Widget sizing: QSizePolicy

QWidget based classes provide the layout management system with size hints. This is a subtle system based on a class named QSizePolicy. A widget's size policy determines how small a widget can shrink, how big it can grow and how big it really wants to be. Then the layout manager negotiates with the widget though the use of the sizeHint() about the size it will get.

A widget can thus indicate whether it prefers to stay a fixed horizontal or vertical size, or would like to grow to occupy all available space. QSizePolicy contains a horizontal size policy record and a vertical size policy record. You can set the size policy programmatically, but the setting is also available in the BlackAdder forms creator.

The following size policies exist:

  • Fixed — the widget can't shrink nor grow.

  • Minimum — the widget can't shrink, and shouldn't grow.

  • Maximum — the widget can't grow, but can shrink without any problem.

  • Preferred — the widget can shrink, but shouldn't grow.

  • MinimumExpanding — the widget can't shrink, but should be allowed to grow as much as possible.

  • Expanding — the widget can shrink, but should be allowed to grow as much as possible.

Groups and frames

One way of getting automatic layout management is by using QFrame, and its children, like QGroupBox. We have already seen such a frame in the radiobuttons example, Example 10-9. The contents of the frame will be managed automatically.

QFrame has three interesting child classes: QGrid, QHBox and QGroupBox. There's also QVBox, which descends from QHBox.

Adding widgets to one of the frame-based layout managers is simply a matter of creating the widget with the layout manager as parent. Those widgets will be resized according to the value their sizeHint() returns.

QHBox

This a very, very simple class. A QHBox aligns its children horizontally, with a settable spacing between them.

QVBox

A QVBox layout is possibly even simpler than the QHBox layout: as the name implies, it aligns its children vertically.

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.

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).

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. It's 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. It's 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 doesn't 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.

QBoxLayout and children

QBoxLayout is the parent class of the horizontal and vertical box layout managers — you will never use this class on its own, but its useful to look at the methods it offers, because those are inherited by QHBoxLayout and QVBoxLayout

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):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        self.setCaption("Adding and deleting widgets")
        self.setName("main window")
        self.mainWidget=QWidget(self) # dummy widget to contain the
                                      # layout manager
        self.setCentralWidget(self.mainWidget)
        self.mainLayout=QVBoxLayout(self.mainWidget, 5, 5, "main")
        self.buttonLayout=QHBoxLayout(self.mainLayout, 5, "button")
        self.widgetLayout=QVBoxLayout(self.mainLayout, 5, "widget")

        self.bnAdd=QPushButton("Add widget", self.mainWidget, "add")
        self.connect(self.bnAdd, SIGNAL("clicked()"),
                     self.slotAddWidget)

        self.bnRemove=QPushButton("Remove widget",
                                  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.

layout.py

setGeometry

You can use setGeometry to set the size of every individual widget yourself. There's 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.

Example 10-13. geometry.py - setting the initial size of an application

#
# geometry.py
#
import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.editor=QMultiLineEdit(self)
        self.setCentralWidget(self.editor)

def main(args):
    app=QApplication(args)
    win=MainWindow()
    win.setGeometry(100,100,300,300)
    win.show()

    app.connect(app, SIGNAL("lastWindowClosed()")
                           , app
                           , SLOT("quit()")
                           )
    app.exec_loop()

if __name__=="__main__":
    main(sys.argv)

Prev | Home | Next
Advanced widgets | Up | Dialogs and Standard Dialogs


Dialogs and Standard Dialogs
Prev Chapter 10. Qt Class Hierarchy Next

Dialogs and Standard Dialogs

Bill Gates is apocryphically reported to have once shouted ‘Why must everyone in my company write his own file-open code? Go and build something that works for every application!". And thus the Windows standard file open dialog was born. This dialog has gone through several versions, necessitating continuous rewriting. All the same, the idea was a good idea, and Qt implements several standard dialogs that look and feel just like the Windows common dialogs, but are a lot easier to program. We'll implement common dialog PyQt lacks, for searching and replacing, in the Section called Non-modal: Search and replace in Chapter 19, Using Dialog Windows.

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.

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.

Example 10-14. dialogs.py - opening message and default dialogs boxes

#
# dialogs.py
#

import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)
        self.setCaption("Network Client")

        self.actionInformation=QAction(self, "Information")
        self.actionInformation.setText("Informational Message")
        self.actionInformation.setMenuText("&Information")
        self.actionInformation.setStatusTip("Show an informational 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 warning mesagebox.")

        self.connect(self.actionWarning,
                     SIGNAL("activated()"),
                     self.slotWarning)

        self.actionCritical=QAction(self, "Critical")
        self.actionCritical.setText("Critical Message")
        self.actionCritical.setMenuText("&Critical")
        self.actionCritical.setStatusTip("Show an informational 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)

        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()
        self.actionAbout.addTo(self.helpMenu)
        self.actionAboutQt.addTo(self.helpMenu)

        self.menuBar().insertItem("&Messages", self.messageMenu)
        self.menuBar().insertItem("&Standard dialogs", self.dialogMenu)
        self.menuBar().insertItem("&Help", self.helpMenu)

    def slotInformation(self):
        QMessageBox.information(self,
                                "Information",
                                "A plain, informational message")

    def slotWarning(self):
        QMessageBox.warning(self,
                            "Warning",
                            "What you are about to do will do some serious harm .")

    def slotCritical(self):
        QMessageBox.critical(self,
                                "Critical",
                                "A critical error has occurred.\nProcessing will be stopped!")

    def slotAbout(self):
        QMessageBox.about(self,
                          "About me",
                          "A demo of message boxes and standard dialogs.")

    def slotAboutQt(self):
        QMessageBox.aboutQt(self)

    def slotFile(self):
        filename=QFileDialog.getOpenFileName("", "*.py", self, "FileDialog")

    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)

Giving the user some information.

A gentle warning

A dire warning

About your application

About Qt

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 it's not something I'd recommend.

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).

QFileDialog

The first of the Qt standard dialogs is the QFileDialog. The file dialog can be 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().

Example 10-15. fragment from dialogs.py - opening a file dialog

...
    def slotFile(self):
        filename=QFileDialog.getOpenFileName("", "*.py", self, "FileDialog")
...

The Qt File dialog

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.

Example 10-16. fragment from dialogs.py - opening a font dialog

...
    def slotFont(self):
        (font, ok) = QFontDialog.getFont(self, "FontDialog")
...

The Qt font dialog

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 won't do for more complex graphics applications (with which you might want to implement something that works with HSV values, or with a color wheel).

Example 10-17. fragment from dialogs.py - opening a color dialog

...
    def slotColor(self):
        color=QColorDialog.getColor(QColor("linen"), self, "ColorDialog")
...

The Qt Color dialog

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, I've never had a need for these. The open remote location dialog in browsers like Opera or Netscape are a common example.

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 won't block the entire application, then it's 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.

Prev | Home | Next
Layout managers | Up | Qt Utility classes and their Python equivalents


Qt Utility classes and their Python equivalents
Prev Chapter 10. Qt Class Hierarchy Next

Qt Utility classes and their Python equivalents

A good many of the Qt classes overlap with Python classes. There is QString, which offers almost as much functionality as Python's string object and Unicode string object, QRegExp, which does the same as Python's re module. Not all Qt classes that overlap with Python classes are available from PyQt, but most are. The defining criterion is usually whether or not such an overlapping class is needed as an argument in method calls.

In those cases where there is duplication, it is up to you to decide which one to use. If you use as many Qt classes as possible, your application will appear less Pythonic, and will be more difficult to port to other Python gui's. However, it will also be easier to port your Python prototype to fast, compiled C++ code. Thus, it depends on whether you see your Python program as the final goal or as a prototype. It is best to take a firm stand, though — you shouldn't use Qt regular expressions and Python regular expressions in the same program.

For instance, the dirview Qt example program can use the QDir class, or use os.path.walk(). Compare the following setOpen functions. The first is a complex script which uses the Qt functions; the second uses setOpen with the Python equivalents. Both scripts create listview items for all entries in a directory:

Example 10-18. from dv_qt.py - using Qt utility classes

#!/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
    self.c = []
    self.readable = 1

  def setOpen(self, o):
    if o and not self.childCount():
      s = self.fullName()
      thisDir = QDir(s)
      if not thisDir.isReadable():
        self.readable = 0
    return

      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

  def text(self, column):
    if column == 0:
      return self.f
    elif self.readable:
      return 'Directory'
    else:
      return 'Unreadable Directory'

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()

Example 10-19. fragment from db_python.py - using Python utility classes

...
  def setOpen(self, o):
    if o and not self.childCount():
      s = self.fullName()

      if (not os.path.isdir(s)):
        self.readable == 0
        return

      if (not os.access(s, os.F_OK or os.R_OK)):
        self.readable == 0
        return

      files=os.listdir(s)
      if files:
        for fileName in files:
          f=os.path.join(s, fileName)
          if fileName == "." or fileName == "..":
            continue
          elif os.path.islink(f):
            d = QListViewItem(self, fileName, 'Symbolic Link')
          elif os.path.isdir(f):
            d = Directory(self, fileName)
          else:

            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 result is the same:

The code is different, but the result the same.

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. Perhaps the similarities between the Qt QDir object and the Python os.path module are even more striking than the differences.

High level data structures

Qt offers a number of custom high-level data structures, where plain C++ is quite primitive in this respect. (Or, at least, C++ was quite primitive in this respect when Qt was first created. Recently, templates and a standard library have been added, and compilers are starting to support these constructs).

Python already has a dictionary, as well as list and string data structures that are powerful enough for most needs (what Python is missing is an ordered dictionary), so PyQt does not need the Qt high-level data structures, except where they are demanded as parameters in methods.

Qt has two basic high-level datastructures: arrays and collections. Collections are specialized into dictionaries, lists and maps. You can always use a Python list wherever a QList is needed, or a string where a QByteArray is needed. In other cases, some methods are not implemented because the datatype is not implemented, as is the case with QMap.

Table 10-1. Qt and Python high-level datastructures

Qt Class | Python Class | Implemented | Description
QArray | list | No | Array of simple types.
QByteArray | String | No | Use a string wherever a method wants a QByteArray
QPointArray | No equivalent | Yes | Array of QPoint objects — you can also store QPoint objects in a Python list, of course.
QCollection | Dictionary | No | Abstract base class for QDict, QList and QMap.
QDict | Dictionary | No | Just like Python dictionaries, but more complicated in use.
QList | list | No | Just like Python lists, but more complicated
QMap | Dictionary | No | Use a Python dictionary — however, when translating a Python prototype to C++, note that a QMap is based on values, not on references; the keys indexing the dictionary are copies of the original objects, not references.
QCache | No equivalent | No | A QCache is a low-level class that caches string values so that two variables containing the same text don't use the memory twice. There are similar caches for integers and non-Unicode texts. Python performs the same trick; see the note: Python and Caching.
QValueList | list | No | A low-level class that implements a list of values (instead of references to objects).
QVariant | No equivalent | Partially | QVariant is a wrapper class that makes it possible to use C++ as if it were a loosely-typed language (which Python already is). This class is used for implementing class properties (I find it to be a monstrosity compared to Visual Basic's Variant type).

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

Python 2.2a4 (#1, Oct  4 2001, 15:35:57)
[GCC 2.95.2 19991024 (release)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> a=1
>>> b=1
>>> id(a)
135268920
>>> id(b)
135268920
>>> a=100
>>> b=100
>>> id(a)
135338528
>>> id(b)
135338504
>>> a="bla"
>>> b="bla"
>>> id(a)
135563808
>>> id(b)
1355638

Files and other IO

Qt's file classes include the following:

  • QDir — directory information

  • QFile — file handling

  • QFileInfo — file information

  • QIODevice — abstract IO device

  • QBuffer — helper class for buffered IO

  • QTextStream — abstract class for text IO

  • QTextIStream — text input IO

  • QTextOSStream — text output IO

  • QAsyncIO — abstract base for asynchronous IO

  • QDataSink — asynchronous data consumer

  • QDataSource — asynchronous data produced of data

  • QDataStream — binary IO

  • QIODeviceSource — a datasource that draws data from a QIODevice, like QFile.

Qt's file and IO handling classes are divided into three groups: classes to handle files on a file system, classes that work on streams, and asynchronous IO classes. We have already seen an example of working with QDir. QFile is Qt's equivalent of the Python file object; it is almost fully implemented in PyQt, with some changes due to naming conflicts. QFileInfo is a useful class that encapsulates and caches information including name, path, creation date and access rights. You could use the various function in Python's os.path module, but those don't cache information.

The base class for all these IO-oriented classes is QIODevice, which is completely implemented in PyQt. You can subclass it for your own IO classes. Qt divides its stream handling into text streams and binary streams. Only the text stream handling in implemented in PyQt, with the QTextStream, QTextIStream and QTextOStream classes. QTextStream is the base class, QTextIStream provides input stream functionality and QTextOStream output stream functionality. One problem remains with these stream classes — operator overloading. C++ relies on the >> and << operators to read from and write to a stream. PyQt doesn't support this yet, so you cannot actually make use of the streaming capabilities. Instead, you will have to limit yourself to using read() to read the entire contents of the stream.

The asynchronous IO classes, the buffer class and the binary IO classes have not been implemented yet. You can easily substitute the various Python modules for file and IO handling. You can use asyncore in place QAsyncIO. The Python file object is buffered by nature, if you open() your files with the bufsize parameter set.

Date and time

  • QDate — representation of a data

  • QTime — clock and time functions

  • QDateTime — combination of QDate and QTime

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.

Mime

  • QMimeSource — a piece of formatted data

  • QMimeSourceFactory — a provider of formatted data

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 drag'n'drop subsystem, in the clipboard handling, and in the production of images for rich text widgets.

An example is given in the application.py script that is included with PyQt. Below, I show a relevant fragment of that example, which takes a bit of html-formatted text with an <img> tag to a certain name, which is resolved using QMimeSourceFactory:

Example 10-20. Using QMimeSourceFactory (application.py)

 ...
fileOpenText = \
'''<img source="fileopen">
Click this button to open a <em>new file</em>.<br><br>
You can also select the <b>Open</b> command from the <b>File</b> menu.'''
...
        QWhatsThis.add(self.fileOpen,fileOpenText)
        QMimeSourceFactory.defaultFactory().setPixmap('fileopen',openIcon)
...

Text handling

  • QString — string handling

  • QRegExp — regular expressions that work on a string.

  • QChar — one Unicode character

  • QValidator — validates input text according to certain rules

  • QTextCodec — conversions between text encodings

  • QTextDecoder — decode a text to Unicode

  • QTextEncoder — encode a text from Unicode

Qt's 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.

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 doesn't mix too well with PyQt — you don't want two threads accessing the same gui element. You're quite safe though, as long as your threads don't access the gui. The next example shows a simple pure-python script with two threads:

Example 10-21. thread1.py — Python threads without gui

#
# thread1.py
#
import sys
import time
from threading import *

class TextThread(Thread):

    def __init__(self, name, *args):
        self.counter=0
        self.name=name
        apply(Thread.__init__, (self, ) + args)

    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.

Example 10-22. Python threads and a PyQt gui window

#
# thread2.py - Python threads
#
import sys, time
from threading import *
from qt import *

class TextThread(Thread):

    def __init__(self, name, *args):
        self.counter=0
        self.name=name
        apply(Thread.__init__, (self, ) + args)

    def run(self):
        while self.counter < 200:
            print self.name, self.counter
            self.counter = self.counter + 1
            time.sleep(1)

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self,) + args)
        self.editor=QMultiLineEdit(self)
        self.setCentralWidget(self.editor)
        self.thread1=TextThread("thread1")
        self.thread2=TextThread("thread2")
        self.thread1.start()
        self.thread2.start()

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)

Another technique (though dangerous!) is to have the GUI use a timer to periodically check the variables produced by the threads. However, concurrent access of a variable can lead to nasty problems.

URL's

The Qt URL-handling classes are quite equivalent to Python's urllib module. It's up to you to choose which you prefer.

  • QUrl

  • QUrlInfo

  • QUrlOperator

Qt modules that overlap with Python modules

In addition, there are two Qt modules that are also available completely as Python modules: the XML module (wrapped by Jim Bublitz) and the Network module. The Python equivalent of Qt's XML module, for simple tasks, xmllib, and for more complicated problems xml.sax. While xmllib is deprecated, it is still very useful and one the simplest ways of handling XML data. The xml.sax module depends on the presence of the expat parser, which is not available on every system.

The Qt network module has not been completely wrapped in PyQt yet. It is mainly useful if you want to program your own network clients and servers, and intend to move your code to C++ one day. Python offers equivalent modules for every service, and a lot more, too (such as http and gopher clients). You might find Qt's socket objects a bit more convenient than Python's offerings. Another possible reason to use the Qt socket classes is that they are better implemented on Windows than the Python socket classes.

Table 10-2. Qt and Python network classes

Qt Class | Python Class | Implemented | Description
QSocket | socket | Partially implemented | Low level network connection object. Note that Python's socket module is useful both for clients and servers, while Qt's QSocket is for clients only: servers use QServerSocket.
QSocketDevice | socket | No | This class is mostly intended for use inside Qt - use QIODevice instead, which is completely implemented in PyQt.
QServerSocket | socket, SocketServer | Partially implemented | The server-side complement of QSocket. Again, Python's socket module servers both for server and client applications. Python offers the server-specific SocketServer module.
QHostAddress | No real equivalent | Partially — not the functionality for IPv6. | A platform and protocol independent representation of an IP address.
QDns | No real equivalent | Not implemented | Asynchronous DNS lookups — it's not implemented, all Python libraries do automatic synchronous DNS lookups, as do the QSocket derived Qt classes.
QFtp | ftplib | Not implemented | This class is seldom used: it's easier to just open an URL either with QUrlOperator or one of the Python Internet protocol handling libraries like urllib.
Prev | Home | Next
Dialogs and Standard Dialogs | Up | Qt Designer, BlackAdder and uic


Advanced Designer topics
Prev Chapter 11. Qt Designer, BlackAdder and uic Next

Advanced Designer topics

In this section, I will discuss more advanced topics in working with BlackAdder Designer. These are: the connecting of the individual widgets in your design using signals and slots, the adding of custom widgets to the BlackAdder Designer palette, and the actual generation and use of code with BlackAdder and with the command-line utility pyuic. Finally, I will give some attention to the generation of C++ code.

Defining signals and slots in Designer

The widgets on a form often have a relationship to each other. For instance, the OK button should ask the form to close. Clicking on a button should move selected items from one QListView to another. As you have seen before, the standard way of linking widgets in PyQt is by connecting signals with slots. You can create these connections by simply drawing a line in BlackAdder designer.

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 two initial 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 ( [image]), 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 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 don't pass on an argument.

Define the following four slots:

  • slotAddAll()

  • slotAddSelection()

  • slotRemoveAll()

  • slotRemoveSelection()

All slots are defined.

Now, you can connect the clicked() signal of each button to the right slot.

All connections are made.

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.

Adding your own widgets

Not only can you add your own slots to the forms you design with BlackAdder, but you can also create custom widgets, and use those widgets in other designs. The design shown in the previous section — two listboxes and a few buttons to move items from left to right, and vice-versa — is something that's quite often needed, and is a prime candidate to turn into a widget.

Open the connections.ui file, and create a new .ui file based on the widget template. Copy everything from the form to the widget, except, of course, the OK, Cancel and Help buttons. Perhaps you will have to do the layout again; if so, use a grid layout. Create the slots again, this time for the widget, and connect them.

The DoubleListBox widget design.

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.

Adding a custom widget.

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 don't, 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 don't 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)

Adding signals to a custom widget.

Note that we give a listviewitem as an argument to these signals; Python signals do have arguments, but they are untyped. Slots are not relevant for this widget, and neither are properties.

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 won't see the widget either; but wen you generate the form, everything will be all right.

A form using the DoubleListBox custom widget..

Layout management

It is possible to design your dialogs and widgets by plonking down elements, sizing them to your liking and placing them where you want, down to the last pixel. If you fix the size of the form and the font, you can have a perfect layout — but it will also be a layout that your users won't like. People want to resize dialogs, either to have more data visible at the same time, or to minimize the amount of space the dialog takes on their already crowded screens. Visually impaired users want to change the font size to something they can see. Furthermore, there are vast differences in default fonts between Windows systems and some other systems, like KDE, which define a different default font dependent upon screen resolution. Your pixel-precise dialog won't look so good if the user views it with a font that he chooses, not in terms of pixel size, but of points per inch — where an inch can have between 75 and 120 pixels. A twelve-point Helvetica has a lot more pixels if generated for a resolution of 120 pixels to the inch, then if it were generated for 75 pixels to the inch.

All these are good reasons to let the computer manage the layout for you. There are other reasons, too. With complex forms, doing the layout yourself quickly becomes a bore. If your application is used by people with a right-to-left alphabet, like Hebrew or Arabic, the whole layout should be mirrored. From version 3, Qt can do 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 won't 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 won't 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 wouldn't appear in the required three seconds.

I've 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.

Layout manager properties.

The Horizontal Layout Manager

The horizontal layout manager lays the widgets out in one row, like the individual menu's in a menu bar, or the buttons at the bottom of a form based on the Dialog With Buttons (Bottom) dialog.

There are few widgets that are customarily laid out horizontally, most often widgets are grouped in vertical columns. The columns themselves can be grouped in a horizontal layout manager.

The Vertical Layout Manager

The vertical layout manager puts widgets in one column. This can be very useful when creating a groupbox that contains radio buttons or checkboxes. You will very seldom want to layout radio buttons in a horizontal row. Another use is the column of buttons in the Dialog with Buttons (right) template.

The Grid Layout Manager

The Grid Layout managers lays out your widgets in a square or oblong grid. If you 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.

The Spacer object

Of course, for all its cleverness, there are situations when the Designer simply cannot determine your meaning without some help. There is no Intention Layout Manager! One useful tool to let the layout manager know your intention is the spacer object. This is an invisible (at runtime) widget that pushes other widgets away. You can use a spacer either horizontally or vertically. If you use a spacer at both sides of a widget, they will push the widget to the middle. If you use only one spacer, it will push the widget to the other side.

Playing with spacers.

What widgets can do to get the space they want

Not every widget wants to hog all the space in a dialog. A combobox, for instance, has no reason to grow vertically, while a vertical scrollbar doesn't need to get any wider. You can set the horizontal and vertical sizepolicies of your widgets in the Designer module.

However, this will not always produce the results you want — in such a case, you might be reduced to setting minimum and maximum pixel widths by hand. This may be necessary if you have a listbox or combobox that expands without limit because one of the entries is as long as a fantasy trilogy without linebreaks. To curb 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 doesn't 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:

  • fixed — what sizeHint() says is law — smaller nor larger is acceptable.

  • minimum — the result of sizeHint() is sufficient. It cannot be smaller, might be larger, but there's no use in growing.

  • maximum — what sizeHint() returns is the max — the widget should not be expanded, but might be shrunk without detriment.

  • preferred — the sizeHint() size is best, but the widget can be smaller without problems. It might be larger, but there's no earthly reason why it should.

  • minimumExpanding — the widget wants as much space as it can get — the more it gets, the better. No way it should shrink.

  • Expanding — the widget wants as much space as it can get, but it's still useful if it get less than the result of sizeHint().

Creating a complex form

Let's try to create a really complicated form, just to see what the automatic layout engine can do.

A quite complex dialog.

This dialog was created in the following steps:

  1. Create a new form — based the simple Dialog Template.

  2. Create two pushbuttons and place them at the right top.

  3. Create a vertical spacer item, below the buttons.

  4. Collect the buttons and the spacer in a rubber band and select the vertical layout manager. Resize the layout to the height of the dialog.

  5. Create the three listboxes, and resize them to roughly about the right size; put the three line editors below.

  6. Select the listboxes and the lineedits in a rubber band, and select the grid layout — resize the complete layout about three-quarters the height of the dialog.

  7. Create a groupbox below the constellation of listboxes and edit controls, and put, roughly vertically, three radio buttons in it.

  8. Select the groupbox and click on the vertical layout manager button. Note that if you have the object browser open, you won't see this layout manager: the groupbox takes that function.

  9. Create two checkboxes, next to each other, below the groupbox.

  10. Select the listboxes, and select the horizontal layout manager.

  11. Now select the form and then the grid layout manager.

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, I'm 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.

Generating and using Python code with pyuic

We have already converted a Designer design to Python code. This can be done using either BlackAdder, with the menu option Compile Form from the File menu, or with a stand-alone utility, such as pyuic.

The stand-alone utility pyuic has an interesting option that is currently not present in the BlackAdder IDE. Using the -x parameter, a small stub is generated at the bottom of the file that enables you to run the generated code directly.

The resulting Python file has all the hallmarks of generated code. That is to say, it is a mess you won't want to edit by hand. Especially since it will be regenerated every time you change your form.

The right way to work with these generated files is to subclass them. If you have created a form, for example frmcomplex.py, that contains the generated class FrmComplex, then your next step is to create a new Python file, dlgcomplex.py, which contains the following class definition:

Example 11-1. dlgcomplex.py — a subclass of frmcomplex.py

#
# dglcomplex.py
#
import sys
from qt import *
from frmcomplex import FrmComplex                            (1)

class DlgComplex (FrmComplex):                               (2)
    def __init__(self, parent = None,name = None,modal = 0,fl = 0):
        FrmComplex.__init__(self, parent, name, fl)          (3)

    def accept(self):                                        (4)
        print "OK is pressed"
        FrmComplex.accept(self)                              (5)

    def reject(self):
        print "Cancel pressed"
        QDialog.reject(self)                                 (6)

if __name__ == '__main__':                                   (7)
    a = QApplication(sys.argv)
    QObject.connect(a,SIGNAL('lastWindowClosed()'),a,SLOT('quit()'))
    w = DlgComplex()                                         (8)
    a.setMainWidget(w)
    w.show()
    a.exec_loop()
Importing the generated class
This form is a subclass of the generated class
By passing self to the parent class, all references in the parent class will be routed via the definitions in the subclass.
Any slot of method of QDialog can be reimplemented in a subclass of QDialog. In this case, the accept() and reject() methods are re-implemented to add custom behavior to the OK and Cancel actions. Remember that we have already created the connections between the clicked() signals of these buttons and these methods in the Designer.
However, if you want to make use of the default functionality of the QDialog class, you must also call the implementation of the subclassed function in the parent class.
Because the generated code in FrmComplex doesn't really add anything, calling QDialog.reject() works just as well.
This is a stub main to test the dialog.
Make sure you instantiate the right class: the DlgComplex, not the frmComplex! Cutting and pasting can lead to difficult-to-find bugs —I have all too often copied the stub from the parent file and forgot to change the classname...

The next move is extending the constructor of the derived class to set the initial values of the various widgets.

Example 11-2. Setting default values

    def __init__(self, parent = None,name = None,modal = 0,fl = 0):
        FrmComplex.__init__(self, parent, name, fl)

        self.ListBox2.insertItem("That's a turnip")
        self.ListBox2.insertItem("Further nonsense")

        self.RadioButton1.setChecked(1)

As you can see, it's 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.

Generating C++ code with uic

Qt is originally a C++ toolkit — and if you acquire a license for Qt, be it the free, GPLed Unix/X11 version or the (non-)commercial Windows/Unix license, you can take the .ui files you have created with BlackAdder and compile them to C++ using the uic utility.

C++ is a bit more complicated than Python, and this is reflected in the more complex procedure you need to follow when converting a .ui to C++. First of all, you need to generate the header files with the uic -o dialog.h dialog.ui command. Next, you generate the actual C++ implementation with the uic -i dialog.h -o dialog.cpp dialog.ui command. The -i tells uic to include the header file, dialog.h.

From that moment on, the work is the same as with Python. You subclass the generated code, adding real implementation logic. Clever usage will include using make to autogenerate the header and implementation files to ensure that the design of the forms in the compiled app always corresponds to the latest designs.

Prev | Home | Next
Qt Designer, BlackAdder and uic | Up | Creating real applications with PyQt


Macro languages
Prev Chapter 12. Application Frameworks Next

Macro languages

Having a separate controller class makes it easy to add more interfaces, for instance a macro-language. While this is quite an advanced topic, and one that merits a chapter in itself (see Chapter 20), you can see for yourself how powerful the current separation is. You can easily create a completely separate script that creates a document and modifies it:

Example 12-2. Scripting an application is easy

#
# Scripting a docview application
#

from docviewdoc import DocviewDoc

doc=DocviewDoc()
doc.slotModify()

You can handle your application's 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

Prev | Home | Next
Application Frameworks | Up | Project layout


Project layout
Prev Chapter 12. Application Frameworks Next

Project layout

A complex project needs to be well-organized in order to be able to find your way in the mess that is created by countless modules, README files, graphics files and license notices. Most likely, your project directory layout will also be the layout of the application when you deliver it to the user. It thus serves a dual purpose: facilitating development and deployment. For deployment, see Chapter 26.

Over the last decade, a standard project layout has grown to maturity for open source projects. This layout consists of a toplevel directory that contains README's, installation instructions, and directories for documentation, data and source code.

A Python project often consists of several modules: one for your document classes, one for custom GUI widgets and one for the application classes, and one for scripts needed to create database definitions. For example:

The toplevel directory layout of a large project

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 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.

Prev | Home | Next
Macro languages | Up | Actions: menus, toolbars and accelerators


Menus
Prev Chapter 13. Actions: menus, toolbars and accelerators Next

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()
        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()

        i=0 # window numbering
        self.menuToWindowMap={}
        for window in self.workspace.windowList():
            i+=1
            index=self.windowMenu.insertItem(("&%i " % i) +
                                             str(window.caption()),
                                             self.slotWindowMenuActivated)
            self.menuToWindowMap[index]=window
            if self.workspace.activeWindow()==window:
                self.windowMenu.setItemChecked(index, TRUE)

    def slotWindowMenuActivated(self, index):
        self.menuToWindowMap[index].setFocus()

We will investigage the creation of application frameworks containing more than one window in chapter Chapter 15.

Prev | Home | Next
Actions: menus, toolbars and accelerators | Up | Toolbars


Toolbars
Prev Chapter 13. Actions: menus, toolbars and accelerators Next

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)
        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 can't 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 ‘what's this' text is added. The result looks like this:

A toolbar with a combobox added.

That ‘what's this' text leads us to a special button offered by PyQt. This is the little arrow-plus-question-mark that activates the ‘What's 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.

Help with an unhelpful icon

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)

If you want to embed a small picture of the icon in the yellow note, you use PyQt's 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 don't 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 it's 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.

Prev | Home | Next
Menus | Up | Keyboard accelerators


Keyboard accelerators
Prev Chapter 13. Actions: menus, toolbars and accelerators Next

Keyboard accelerators

The old vi or Emacs lags amongst us know this already, just as the WordPerfect veterans know: regardless of what Rob Pike, designer of the Plan9 operating system, says, using a keyboard can actually be preferable to grabbing the mouse for every trifle. Giving the vast majority of interface actions in your application a keyboard accelerator is therefore a Good Thing, and is highly encouraged.

Note the subtle difference between a keyboard accelerator and a keyboard shortcut. Shortcuts are generally combinations of the Alt key and a letter. They are indicated in menus and dialogs by an underscore, and created by simply prefixing an ampersand (&) to the shortcut letter. Shortcuts only function in the context of a menu or dialog box: that's why Alt-F generally opens the file menu, but Alt-Q doesn't 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 PyQt's 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.

Prev | Home | Next
Toolbars | Up | Setting an application icon


Setting an application icon
Prev Chapter 13. Actions: menus, toolbars and accelerators Next

Setting an application icon

The icing on the cake: of course you want your application to be represented by its very own icon. However, this is not very simple. You first need to get a friendly artist to create a nice icon for you, and set the icon of the toplevel widget to it:

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))

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.

Prev | Home | Next
Keyboard accelerators | Up | Automatic testing with PyUnit


Starting out
Prev Chapter 14. Automatic testing with PyUnit Next

Starting out

First, you need a copy of unittest.py: this is included with Python version 2.1, or available separately from http://pyunit.sourceforge.net. Together with unittest.py there is a module unittestgui.py, which implements a simple Tkinter interface to the testing framework.

Since the unittest gui isn't always available — not every Python installation includes Tkinter —I have rewritten the framework to use PyQt, and the PyQt version is available with this book. This version is also more stable when testing classes derived from QObject— the Tkinter unittest GUI has a nasty tendency to crash when running more than one test that's based on a QObject. The Qt unittest gui is a drop-in replacement, and the filename is unittestgui.py.

pyunit.py needs to be on your Python path; additionally, you might want to execute unittestgui.py by itself. It is often comfortable to create either an icon on your desktop for unittestgui.py, or to write a small startup script in your $HOME/bin directory.

Once you have the supporting odds and ends in place, you can start writing tests for your application.

Prev | Home | Next
Automatic testing with PyUnit | Up | A first testcase


A first testcase
Prev Chapter 14. Automatic testing with PyUnit Next

A first testcase

A testcase is a single class that contains one or more methods that perform a single check each. It is possible to check two different behaviors in one test method, but that's a particularly bad idea— if the test fails, you won't know exactly what went wrong, and where.

The example class we want to test, DocviewDoc, is to show a very simple behavior: it must be creatable, and it must know whether it is modified. That makes for two tests.

To create a test, we have to subclass the unittest.TestCase class. This class provides some methods for the setting up and destroying of a test environment and a default method, runTest, that can contain the testing code. A very simple test is defined as follows:

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, let's define a testcase:

#
# dvt1.py - a simple test of instantiating a document
#
import unittest
from docviewdoc import DocviewDoc

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 don't have to write them if you don't 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 Python's standard assert statement, a check is made to see whether the variable doc still points to None—if 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 isn't 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.

Python's assert statement is very handy, and quite simple. It takes the form of:

      assert expression, message

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 shouldn't 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.

Prev | Home | Next
Starting out | Up | Collecting tests in a test suite


Collecting tests in a test suite
Prev Chapter 14. Automatic testing with PyUnit Next

Collecting tests in a test suite

Executing this script isn't much use. First, the test must be collected in a testsuite, and the testsuite must then be run. A testsuite is a collection of related tests, of the type TestSuite.

It is customary to create a function suite() in every testfile that return a suite with all the tests the file contains. This function is then called by a TestRunner object. This will be either a TextTestRunner, if you run your tests without a gui, or a gui TestRunner application, such as the QtTestRunner. Defining a testsuite is a simple matter of creating a TestSuite object and adding tests:

def suite():
    testSuite=unittest.TestSuite()
    testSuite.addTest(DocviewDocTestCase())
    return testSuite

In order to be able to execute this file it's handy to add a bit of executable code to the end:

def main():
    runner = unittest.TextTestRunner()
    runner.run(suite())

if __name__=="__main__":
    main()

Running the test will give you the satisfaction of knowing that at least one piece of your application is working:

boud@calcifer:~/doc/pyqt > python ch9/dvt1.py
runTest (__main__.DocviewDocTestCase) ...
ok
------------------------------------------------------------------------------
Ran 1 test in 0.035s

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:

Preparing a GUI test run.

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 that's included with Python 2.2.

Prev | Home | Next
A first testcase | Up | A more complicated test


A more complicated test
Prev Chapter 14. Automatic testing with PyUnit Next

A more complicated test

Remember, we have two tests to perform on the DocviewDoc class. It would be a 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 that's 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 reason: " +
                 sys.exc_info()[0])
        else:
            assert doc!=None, 'Could not instantiate DocviewDoc'

    def checkModifiable(self):
        """Check whether the document could be modified"""
        doc=DocviewDoc()
        doc.slotModify()
        assert doc.isModified(), 'Document could not be modified'

    def checkUniverse(self):
        """Check whether the universe is still sane"""
        try:
            val = 1 / 0
        except ZeroDivisionError:
            pass # all natural laws still hold
        else:
            fail ("The universe has been demolished and replaced 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()

In this case, DocviewDocTestCase contains two tests: checkInstantion and checkModifiable. This means that two instances of DocviewDocTestCase are added to the testsuite.

I've also added a small test of the universe, to show how to test that your exceptions are fired when you feed your classes illegal input. There are many cases in which you want your code to raise an exception, rather than silently continuing to churn out illegal data. In those cases, you will want the test to succeed when the exception is raised. On the other hand, if the exception is not raised, something happens, and the test fails. That's exactly what the try...except...else block in testUniverse does.

You can thus use the fail() function to let a test noisily fail with a message. There are two similar functions: failIf() and failUnless(), that cause the test to fail if the tested expression is true and if the tested expression is false, respectively:

    def checkFailUnless(self):
        self.failUnless(1==1, "One should be one.")

    def checkFailIf(self):
        self.failIf(1==2,"I don't one to be one, I want it to be two.")

A shorter way to check that an exception is indeed raised, is to use assertRaises:

def divide(a, b):
    return a/b
...
def checkShortCircuitException(self):
    self.assertRaises(ZeroDivisionError, divide, 1, 0)

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.

Prev | Home | Next
Collecting tests in a test suite | Up | Large projects


Large projects
Prev Chapter 14. Automatic testing with PyUnit Next

Large projects

In large projects, where you have many tests, you will want to automate the assembly of testsuite as much as possible. By creating a few Python scripts that work with standardized testsuites (e.g., the function that returns the testsuite is always module.suite()), you can run all tests as often as you want to.

You can already nest testsuites out of the box, and by creating a master test class that reads a configuration file and constructs a master test-suite, you can test a whole system in one run.

Take the following definition file, for instance:

#
# unittests - unittests for the whole system.
#
# dvt1 tests the creation of a docviewdoc
#dvt1.suite
# dvt2 tests a whole lot more
dvt2.suite

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()

Note the use of the function unittest.createTestInstance, which can create a testcase or testsuite from a simple string. There's an optional second argument, module, which points to the module where the test can be found.

Another function, unittest.makeSuite() can scan a class for functions that begin with a certain prefix, and combine them into a testsuite. For instance, we could rewrite dvt2.py into:

#
# dvt3.py - using makeSuite
#
import sys
import unittest
from docviewdoc import DocviewDoc

def divide(a, b):
    return a/b

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(), 'Document 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.

Prev | Home | Next
A more complicated test | Up | Testing signals and slots


Testing signals and slots
Prev Chapter 14. Automatic testing with PyUnit Next

Testing signals and slots

It's quite difficult to work the signals and slots mechanism into the unittest framework. This is not surprising, since signals and slots quintessentially join components together, and the unittests are meant to test each component separately.

However, you might want to test whether calling a method on a certain object causes it to emit the right signals. We need a bit of a custom framework for that purpose, a kind of signal test.

You can use the ConnectionBox from the following script for that purpose. It is a simple class, derived from QObject, which has one slot, slotSlot(), that can be connected to a signal with any number of arguments.

The arguments to the signal are stored in the ConnectionBox, so they can be checked later using the various assertion functions.

I have provided three assertion functions, one to check whether the signal did arrive (assertSignalArrived), one to check whether the number of arguments was right, (assertNumberOfArguments), and one to check the types of the arguments using the Python types (assertArgumentTypes). This provides typenames for all built-in types, but objects created from all user-defined classes (including PyQt classes), belong to the InstanceType. This means that you cannot check whether you got a QListViewItem or a QListView from a PyQt signal using this function.

It would be a nice exercise to extend this assert with checking objects using the QObject.className() method. Feel free...

#
# signals.py - unit-testing signals
#
import sys
import unittest
import types
from docviewdoc import DocviewDoc
from qt import *

class ConnectionBox(QObject):

    def __init__(self, *args):
        apply(QObject.__init__,(self,)+args)
        self.signalArrived=0
        self.args=[]

    def slotSlot(self, *args):
        self.signalArrived=1
        self.args=args

    def assertSignalArrived(self, signal=None):
        if  not self.signalArrived:
            raise AssertionError, ("signal %s did not arrive" % signal)

    def assertNumberOfArguments(self, number):
        if number <> len(self.args):
            raise AssertionError, \
                  ("Signal generated %i arguments, but %i were expected" %
                                    (len(self.args), number))

    def assertArgumentTypes(self, *args):
        if len(args) <> len(self.args):
            raise AssertionError, \
         ("Signal generated %i arguments, but %i were given to this function" %
                                 (len(self.args), len(args)))
        for i in range(len(args)):
            if type(self.args[i]) != args[i]:
                raise AssertionError, \
                      ( "Arguments don't match: %s received, should be %s." %
                                      (type(self.args[i]), args[i]))

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
        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 signal 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 signal 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 signal has the right
           type of arguments.
        """
        self.connectionBox.connect(self.doc, PYSIGNAL("sigDocModified"),
                                   self.connectionBox.slotSlot)
        self.doc.slotModify()
        self.connectionBox.assertArgumentTypes(types.IntType)

def suite():
    testSuite=unittest.makeSuite(SignalsTestCase, "check")
    return testSuite

def main():
    runner = unittest.TextTestRunner()
    runner.run(suite())

if __name__=="__main__":
    main()

Using this ConnectionBox, you can test your signals:

boud@calcifer:~/doc/pyqt/ch9 > python signals.py
Check whether the sigDocModified signal has the right number arguments ... ok
Check whether the sigDocModified signal has the right type of arguments ... ok
Check whether the sigDocModified signal arrives ... ok
Check whether the sigDocModifiedXXX signal does not arrive ... ok
------------------------------------------------------------------------------
Ran 4 tests in 0.003s

OK

Prev | Home | Next
Large projects | Up | Conclusion


Conclusion
Prev Chapter 14. Automatic testing with PyUnit Next

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 means productive. If your way of working helps you make less mistakes, the productivity benefit is enormous. Keep that bar green!

Prev | Home | Next
Testing signals and slots | Up | A More Complex Framework: Multiple Documents, Multiple Views


Document/View Manager
Prev Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views Next

Document/View Manager

Let's first take stock of the requirements for a document/view manager, and write a little testcase.The document/view manager will have to take care of the mapping between documents and views. Every document can have more than one view, but a view can only show one document.

  • The document/view manager must be able to create new documents, together with new views.

  • The document/view manager will have to make sure the document gets closed when the last view is closed.

  • The document/view manager must be able to create new views for existing documents.

  • The document/view manager must not be forced to know about exact document or view classes, but work against a standard set of methods, i.e., an interface.

A guideline during implementation is that the document/view manager should not make GUI calls directly. There are two reasons for this: it is easier to write good testcases if there is no GUI involved, and denying the document/view manager access to the GUI forces us to place all the actual GUI code in one place, namely the application or controller object.

Here's the testcase:

Example 15-1. A testcase for a document manager

import unittest
from docmanager import *
from qt import *

class TestViewManager(QObject):

    def activeWindow(self):
        return None

    def width(self):
        return 100

    def height(self):
        return 100

class TestParent(QObject):

    def queryCloseDocument(self, document):
        return QMessageBox.Yes

    def querySaveDocument(self, document):
        return QMessageBox.No

    def queryDiscardDocument(self, document):
        return QMessageBox.Yes

    def queryFileName (self, document =None):
        return "fileName"

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"

    def setPathName(self, pathname):
        pass

class TestView(QObject):

    def __init__(self, parent, document, *args):
        QObject.__init__(self, parent)
        self._document = document

    def show(self): pass

    def showMaximized(self): pass

    def setCaption(self, caption): pass

    def resize(self, x, y): pass

    def close(self, destroy):
        return TRUE

The purpose of this testcase is to test the documentmanager. An interesting side effect is that the development of the testcase necessitates the development of fake versions of the other necessary components. Creating these fake components for the view, document and application makes clear which functions they must support.

The TestViewManager class is an interesting object. It will manage the different views (windows, tabs, or splitpanes) for the application. As such, it will become the visual counterpart of the DocManager class.

The TestParent represents the application itself — that is, the central class that manages all QActions, menus, toolbars and so on. As you can see, we need four methods in the application, to ask the user whether she wants to close, save or discard the document, and what the filename should be. By not calling QMessageBox or QFileDialog directly, we again get a stronger separation of GUI and application logic. But life is messy, and a complete separation is not attainable.

This is most apparent in the TestView class. Here we need to create stubs for a number of functions that are part of QWidget, such as setCaption().

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 = DocManager(self.parent, self.viewManager)
        except Exception, e:
            self.fail("Could not instantiate docmanager: " + str(e))

    def checkCreateDocument(self):
        docManager = DocManager(self.parent, self.viewManager)
        numberOfDocs = docManager.numberOfDocuments() + 1
        numberOfViews = docManager.numberOfViews() + 1
        try:
            document = docManager.createDocument(TestDocument, TestView)
        except Exception, e:
            self.fail("Could not add a new document: " + str(e))

        assert document, "No document created"
        assert numberOfDocs == docManager.numberOfDocuments(),\
               "No document added"
        assert numberOfViews == docManager.numberOfViews(), \
               "No view added"
        assert docManager.views(document),\
               "Document does not have a view"

    def checkAddView(self):
        docManager = DocManager(self.parent, self.viewManager)
        document = docManager.createDocument(TestDocument, TestView)
        numberOfDocs = docManager.numberOfDocuments()
        numberOfViews = docManager.numberOfViews() + 1
        numberOfDocViews = len(docManager.views(document)) +1

        try:
            view = docManager.addView(document, TestView)
        except DocManagerError, e:
            self.fail(e)
        except Exception, e:
            self.fail("Could not add a view to a document " + str(e))

        assert view is not None,\
               "No view created"
        assert numberOfDocs == docManager.numberOfDocuments(),\
               "Document added"
        assert numberOfViews == docManager.numberOfViews(), \
               "No view added"
        assert numberOfDocViews == len(docManager.views(document)), \
               "No view added to document"

        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 = docManager.createDocument(TestDocument, TestView)
        view = docManager.addView(document, TestView)
        numberOfViews = docManager.numberOfViews()
        docManager.closeView(view)
        assert numberOfViews > docManager.numberOfViews(), \
               "No view removed: was %i, is %i" % (docManager.numberOfViews(),
                                                   numberOfViews)

    def doNotCheckCloseDocument(self):
        docManager = DocManager(self.parent, self.viewManager)
        document = docManager.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()

A look at the testcases shows how the documentmanager is intended to be used. When a document is created, one view is automatically created. More views can be added to a document. Views can be removed, and when the last view is removed, the document is closed. Creating documents and views is the job of the documentmanager; this is why we pass the classes of the view and document to the manager, and not to complete objects.

As I said, life is messy, and if you look at the last test, you will see one bit of unavoidable mess. During the implementation of the document manager it became clear that in order to ‘catch' events (such as closing the application or window with the close button in the title bar) it was necessary to install an event filter in every view. This meant that the original implementation of closeDocument(), which called closeView(), had to be changed to one where closeDocument() called view.close() — which fires the event filter, which fires the closeView(). This, however, is only possible if you use actual QWidget-derived objects; it cannot be done with the fakes we created for the test. This means that the checkCloseDocument() needs to be renamed doNotCheckCloseDocument()(It is a convention to prefix tests that don't work with doNot)—the test will never work.

Prev | Home | Next
A More Complex Framework: Multiple Documents, Multiple Views | Up | The Document Manager


The Document Manager
Prev Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views Next

The Document Manager

The DocManager class is one of the more complex classes discussed in this book.

Example 15-2. The document manager class

"""
    docmanager.py — manager class for document/view mappings

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl

"""

from qt import *

TRUE=1
FALSE=0

class DocManagerError(Exception):pass

class NoSuchDocumentError(DocManagerError):

    ERR = "Document %s with title %s is not managed by this DocumentManager"

    def __init__(self, document):
        self.errorMessage = ERR % (str(document), document.title(), str())

    def __repr__(self):
        return self.errorMessage

    def __str__(self):
        return self.errorMessage

class DocumentsRemainingError(DocManagerError):

    def __init__(self, document):
        self.errorMessage = "There are still documents remaining."

    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 removal of documents
    and views.
    """
    def __init__(self, parent, viewManager = None):
        QObject.__init__(self)
        self._viewToDocMap = {}
        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 shouldn't 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)

    def views(self, document):
        return self._docToViewMap[document]

    def _createView(self, document, viewClass):
        view = viewClass(self._viewManager,
                         document,
                         None,
                         QWidget.WDestructiveClose)
        view.installEventFilter(self._parent)
        if self._viewToDocMap == {}:
            view.showMaximized()
        else:
            view.show()
        if self._docToViewMap.has_key(document):
            index = len(self._docToViewMap[document]) + 1
        else:
            index = 1
        view.setCaption(document.title() + " %s" % index)
        return view

The function _createView(self, document, viewClass) not only maps views to documents, but also creates the view objects. Note the QWidget.WDestructiveClose flag — if this is not passed to the QWidget-derived view class, the view will not disappear from the screen when closed! If this view is the first, then it it will be maximized. This is one area where the docmanager still assumes a traditional MDI paradigm — we'll massage this out in the next chapter. Note also that we keep count of the number of views in each document, and then set the caption accordingly.

Note also that we ‘install' the event filter of the parent object — that is, the application — in the view. This overrides the default event handling of the view object, and makes it possible to use the document manager object.

    def createDocument(self, documentClass, viewClass):
        document = documentClass()
        view = self._createView(document, viewClass)
        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 document

The createDocument(self, documentClass, viewClass) command actually instantiates the document. When that's done, a view is created and mapped to the document. Note the signal we emit here: it can be useful for the application object to know that the number of documents has been changed. For instance, the "save" menu option must be enabled when the first document is created.

    def addView(self, document, viewClass):
        if self._docToViewMap.has_key(document):
            view = self._createView(document, viewClass)
            self._docToViewMap[document].append(view)
            self._viewToDocMap[view] = document
            return view
        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 doesn't belong to this manager.

    def addDocument(self, document, viewClass):
        view = self._createView(document, viewClass)

        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

Of course, it must be possible to add an existing document to the document manager. This is used when the user opens a document.

    def activeDocument(self):
        if self._viewManager.activeWindow() is not None:
            return 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.

    def _saveDocument(self, document):
        if document.pathName() == None:
            document.setPathName(self._parent.queryFileName(document))
        try:
            document.save()
        except Exception, e:
            QMessageBox.critical(self,
                                 "Error",
                                 "Could not save the current 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.

    def _queryCloseDocument(self, document):
        if self._parent.queryCloseDocument(document) == QMessageBox.No:
            return FALSE
        if document.modified():
            save = self._parent.querySaveDocument(document)
            if save == QMessageBox.Yes:
                try:
                    self._saveDocument(document)
                    return TRUE
                except Exception, e:
                    if self._parent.queryDiscardDocument(document) <> \
                       QMessageBox.Yes:
                        return FALSE
                    else:
                        return TRUE
            elif save == QMessageBox.No:
                return TRUE
            elif save == QMessageBox.Cancel:
                return FALSE
        return TRUE

The aim of _queryCloseDocument is to determine what the user really wants when he closes a document—an action that can throw quite a few wobblies. At every step the function asks the user what he wants. Does he want to save the data? And in case saving doesn't succeed, does he want to discard the document? Or would he prefer to keep the document open, and go on throwing foul looks at an application that contains his precious data, which he cannot save?

    def _removeView(self, view, document):
        try:
            self._docToViewMap[document].remove(view)
            del self._viewToDocMap[view]
        except ValueError, e:
            pass # apparently already deleted

    def closeView(self, view):
        document=self._viewToDocMap[view]
        if len(self._docToViewMap[document])==1:
            if self._queryCloseDocument(document):
                self._removeView(view, document)
                del self._docToViewMap[document]
                return TRUE
            else:
                return FALSE
        else:
            self._removeView(view, document)
            return TRUE

    def closeDocument(self, document):
        l=self._docToViewMap[document][:]
        for view in l:
            if view.close(TRUE) == FALSE:
                return FALSE
        self.emit(PYSIGNAL("sigNumberOfDocsChanged"),())
        return TRUE

    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).

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.

It's an interesting exercise to follow this happening with the BlackAdder debugger.

Prev | Home | Next
Document/View Manager | Up | Document


Document
Prev Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views Next

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.

Example 15-3. The document class

"""
mdidoc.py — document or application model.

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl

"""
from qt import *
from resources import TRUE, FALSE

class MDIDoc(QObject):
    """
    The document represents the application model. The current
    document keeps a 'modified' state.

    signals: sigDocModified (boolean)
             sigDocTitleChanged (string)

    """
    def __init__(self, *args):
        apply(QObject.__init__, (self,)+args)
        self.newDocument()
        self._fileName=None
        self._title="Untitled"

    def setPathName(self, fileName):
        self._fileName=fileName
        self.setTitle(str(QFileInfo(fileName).fileName()))

    def pathName(self):
        return self._fileName

    def setTitle(self, title):
        self._title=title
        self.emit(PYSIGNAL("SigDocTitleChanged"),
                  (self._title,))

    def title(self):
        return self._title

    def newDocument(self):
        self.slotModify(FALSE)

    def open(self, fileName, format=None):
        self.slotModify(FALSE)
        self.setPathName(fileName)

    def slotModify(self, value=None):
        if value==None:
            self._modified=not self._modified
        else:
            self._modified = value
        self.emit(PYSIGNAL("sigDocModified"),
                  (self._modified,))

    def modified(self):
        return self._modified

    def close(self):
        pass

    def save(self, fileName = None, format = None):
        if fileName is not None and fileName <> "":
            self.setPathName(fileName)
        else:
            if self.pathName() == None:
                raise "Could not save document: no filename."
        self.slotModify(FALSE)

Prev | Home | Next
The Document Manager | Up | View


View
Prev Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views Next

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 QWidget's default functionality. Close events are handled by the application itself, not by the view. The view doesn't know anything about the document manager, which ultimately handles the close event for all views.

Example 15-4. The view class

"""
mdiview.py — view component

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *

class MDIView(QWidget):
    """
    The MDIView class can represent object of class
    MDIDoc on screen.

    slots:
          slotDocModified
    """
    def __init__(self, parent, doc, *args):
        apply(QWidget.__init__,(self, parent) + args)
        self.doc = doc
        self.connect(self.doc, PYSIGNAL("sigDocModified"),
                     self.slotDocModified)
        self.connect(self.doc, PYSIGNAL("sigDocTitleChanged"),
                     self.setCaption)
        # Set initial values
        self.slotDocModified(self.doc.modified())

    def slotDocModified(self, value):
        if value:
            self.setBackgroundColor(QColor("red"))
        else:
            self.setBackgroundColor(QColor("green"))

    def mouseDoubleClickEvent(self, ev):
        self.doc.slotModify() # direct call to the document

    def document(self):
        return self.doc

    def closeEvent(self, e):
        pass

    def close(self, destroy=0):
        return QWidget.close(self, destroy)

Prev | Home | Next
Document | Up | The actual application


The actual application
Prev Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views Next

The actual application

As with the document-view framework, you can view the QMainWindow derived class as the central application controller. It takes the user input and translates that to calls to either the data or the GUI interface.

Even though the MDIApp class might appear a bit complex (and certainly very long!) it is much simpler than it would be with everything from the DocManager added to it. The creation of QActions, and the attendant fringe decorations such as menu's and toolbars, is quite standard:

Example 15-5. The application class

"""
mdiapp.py — application class for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""

from qt import *

from mdiview import MDIView
from mdidoc import MDIDoc
from docmanager import DocManager

from resources 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
    #

    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)

        self.actions["fileSaveAs"] = QAction("Save as",
                                             fileSaveIcon,
                                             "&Save as",
                                             QAccel.stringToKey(""),
                                             self)
        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")
        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"] = QActionGroup(self, None, FALSE)
        self.actions["windowAction"].insert(self.actions["windowCloseWindow"])
        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. We'll be adding these over the next few chapters.

    def initMenuBar(self):
        self.fileMenu = QPopupMenu()
        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 don't 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 managed by the document manager. This is done in the slotWindowMenuAboutToShow slot function.

    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)

If there is no document loaded by the application, functions like ‘save', ‘close' or ‘edit' are not terribly relevant. It's better to disable them then. By requesting the number of documents managed by the document manager, we can easily achieve this. After all, no documents is zero, which is false for Python, and more than zero documents is always true.

The next section is concerned with the implementation of the slots called by the QAction objects that we just created:

    #
    # Slot implementations
    #

    def slotFileNew(self):
        document = self.docManager.createDocument(MDIDoc, MDIView)

Creating a document is now simply a matter of asking the document manager to do it — just as we did in the test script.

    def slotFileOpen(self):
        fileName = QFileDialog.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.

    def slotFileSave(self, document=None):
        if document == None:
            document = self.docManager.activeDocument()
        if document.pathName() == None:
            self.slotFileSaveAs()
        else:
            try:
                document.save()
            except Exception, e:
                QMessageBox.critical(self,
                                     "Error",
                                     "Could not save the current document")

    def slotFileSaveAs(self, doc=None):
        fileName = QFileDialog.getSaveFileName(None, None, self)
        if not fileName.isEmpty():
            if doc == None:
                doc = self.docManager.activeDocument()
            try:
                doc.save(str(fileName))
            except:
                QMessageBox.critical(self,
                                     "Error",
                                     "Could not save the current document")

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):
        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 templates.\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)
        self.windowMenu.insertSeparator()

        i=0 # window numbering
        self.menuToWindowMap={}
        for window in self.workspace.windowList():
            i+=1
            index=self.windowMenu.insertItem(("&%i " % i) +
                                             str(window.caption()),
                                             self.slotWindowMenuActivated)
            self.menuToWindowMap[index]=window
            if self.workspace.activeWindow()==window:
                self.windowMenu.setItemChecked(index, TRUE)

    def slotWindowMenuActivated(self, index):
        self.menuToWindowMap[index].setFocus()

Here, we dynamically create the window menu just before it is shown. The four menu options—new window, cascade, tile and close—are 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
    #

    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()
                    event.accept()
                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 false—true 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 (that's 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

    def querySaveDocument(self, document):
        r = QMessageBox.information(self,
                                    str(self.caption()),
                                    "Do you want to save your changes to " +
                                    "%s?" %
                                    document.title(),
                                    "Yes",
                                    "No",
                                    "Cancel",
                                    0, 2)
        if r == 0:
            return QMessageBox.Yes
        elif r == 1:
            return QMessageBox.No
        else:
            return QMessageBox.Cancel

    def queryDiscardDocument(self, document):
        r = QMessageBox.warning(self,
                                str(self.caption()),
                                "Could not save %s.\n" % document.title() +
                                "Do you want to discard your changes?",
                                "Yes",
                                "No",
                                None,
                                0, 1)
        if r == 0:
            return QMessageBox.Yes
        else:
            return QMessageBox.No

    def queryFileName (self, document=None):
        fileName = QFileDialog.getSaveFileName(None, None, self)
        if not fileName.isEmpty():
            return str(fileName)
        else:
            return "untitled"

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. That's why I translate the position of the button pressed to the correct identity.

Prev | Home | Next
View | Up | Conclusion


Conclusion
Prev Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views Next

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.

Prev | Home | Next
The actual application | Up | User Interface Paradigms


Back to the MDI windows
Prev Chapter 16. User Interface Paradigms Next

Back to the MDI windows

Changing the document manager and the application object to work with the TabManager class makes them unable to work with the vanilla PyQt QWorkspace class. We will have to wrap this one in a class of our own that sports the same functions as the TabManager class. Fortunately, this is not an onerous exercise.

"""
workspace.py - MDI workspace class for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class WorkSpace(QWorkspace):

    def __init__(self, *args):
        apply(QWorkspace.__init__,(self, ) + args)

    def addView(self, view): pass

    def removeView(self, view): pass

    def canCascade(self):
        return TRUE

    def canTile(self):
        return TRUE

That didn't hurt, did it? We added a mere four functions to the interface for our view managers. Adding and removing a view from a workspace doesn't 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 don't need to reimplement them.

Prev | Home | Next
User Interface Paradigms | Up | A row of split windows


A row of split windows
Prev Chapter 16. User Interface Paradigms Next

A row of split windows

We'll make a little viewmanager class will arrange the views separated by splitter bars. That class will be based on the QSplitter class. I would advise you never to use a splitter to separate documents (in contrast to BlackAdder, which does use this paradigm) — it's so uncommon that people will get confused. Building the class is a useful little introduction to QSplitter, though. A splitter is best used if you have a list of items or icons on the left, and a document pane to the right. Indeed, you might want to use one of the other arrangements for showing more than one window in the document pane, and separate the workspace from a selection list using a QSplitter. If you do this, the selection list functions as a kind of always-visible windows menu.

"""
splitspace.py - splitter view manager for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE
class SplitSpace(QSplitter):

    def __init__(self, *args):
        apply(QSplitter.__init__,(self, ) + args)
        self.views=[]

    def addView(self, view):
        self.views.append(view)

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 removeView(self, view): pass

    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 cascade(self): pass

    def tile(self): pass

    def canCascade(self):
        return FALSE

    def canTile(self):
        return FALSE

Obviously, cascading nor tiling is relevant for this class.

Prev | Home | Next
Back to the MDI windows | Up | A stack of documents


A stack of documents
Prev Chapter 16. User Interface Paradigms Next

A stack of documents

I said I wouldn't do an emacs-like stack of documents without any surrounding GUI — but it was so easy. PyQt contains a very basic class, QWidgetStack, which can contain any number of widgets, though only one is shown at a time. This class is used in QWizard, QTabWidget and QTabDialog, but it can be very useful when used by itself, too.

"""
stackspace.py - stacked view manager for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class StackSpace(QWidgetStack):

    def __init__(self, *args):
        apply(QWidgetStack.__init__,(self, ) + args)
        self.views=[]

    def addView(self, view):
        self.views.append(view)
        self.addWidget(view, len(self.views) - 1)
        self.raiseWidget(view)

    def removeView(self, view):
        if view in self.views:
            self.views.remove(view)
            self.removeWidget(view)

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()

    def cascade(self): pass

    def tile(self): pass

    def canCascade(self):
        return FALSE

    def canTile(self):
        return FALSE

    def windowList(self):
        return self.views

    def activateView(self, view):
        self.raiseWidget(view)

In contrast with all other view managers we have created up to now, QWidgetStack does not automatically raise a window when it gets focus. This means that we have to add a new method to the view manager interface— activateView. This has to be added to all other view managers, too, and there is a small change necessary in the application class MDIApp:

    def slotWindowMenuActivated(self, index):
        self.menuToWindowMap[index].setFocus()

becomes:

    def slotWindowMenuActivated(self, index):
        self.workspace.activateView(self.menuToWindowMap[index])

Of course, this is merely an example of the use of QWidgetStack. If you want to present your users with stacked document windows, you ought to offer more than a mere ‘window' menu for selecting windows— A keyboard interface, for instance, or perhaps a listview with icons for open documents to the left.

Prev | Home | Next
A row of split windows | Up | A more complex view management solution


A more complex view management solution
Prev Chapter 16. User Interface Paradigms Next

A more complex view management solution

In the previous section I suggested that it might be nice to code up a view manager where the list of open views was shown in a listbox on the left side. I could have have left this for you to do, but I couldn't resist.

"""
listspace.py - stacked view manager with a list for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

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.

    def __init__(self, *args):
        apply(QSplitter.__init__,(self, ) + args)
        self.viewlist=QListBox(self)
        self.setResizeMode(self.viewlist,
                           QSplitter.KeepSize)
        self.stack=QWidgetStack(self)
        self.views=[]
        self.connect(self.viewlist,
                     SIGNAL("highlighted(int)"),
                     self.__activateViewByIndex)

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 header or multi-column capabilities of QListView, the change to QListView will be simple enough.

Because the highlighted(int) signal of QListBox passed the index of the selected entry in the listbox, not the actual view object, we have to pass it through an internal function, __activateViewByIndex, which maps the index to the view object that should be activated.

    def addView(self, view):
        self.views.append(view)
        self.viewlist.insertItem(view.caption(), len(self.views) - 1)
        self.stack.addWidget(view, len(self.views) - 1)
        self.stack.raiseWidget(view)
        self.connect(view,
                     PYSIGNAL("sigCaptionChanged"),
                     self.setListText)

    def setListText(self, view, caption):
        i = self.views.index(view)
        self.viewlist.changeItem(caption, i)

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.

    def removeView(self, view):
        if view in self.views:
            self.viewlist.removeItem(self.views.index(view))
            self.stack.removeWidget(view)
            self.views.remove(view)

Removing an item from a QListView is rather difficult to do without clearing the entire listview and building the contents anew. [1] Fortunately, the QListBox class offers a handy remove() function.

    def activeWindow(self):
        return self.stack.visibleWidget()

    def cascade(self): pass

    def tile(self): pass

    def canCascade(self):
        return FALSE

    def canTile(self):
        return FALSE

    def windowList(self):
        return self.views

    def activateView(self, view):
        self.stack.raiseWidget(view)

    def __activateViewByIndex(self, index):
        self.activateView(self.views[index])

Apart from __activateViewByIndex(), which we discussed above, the rest of the code is a plain reflection of our view manager API — in other words, nothing spectacular.

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 won't make any difference—as 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.

Prev | Home | Next
A stack of documents | Up | Conclusion


Conclusion
Prev Chapter 16. User Interface Paradigms Next

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 doesn't quite fit into the framework we developed in this chapter, unfortunately.

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 we've added some functionality to the application in the next chapter.

Prev | Home | Next
A more complex view management solution | Up | Creating Application Functionality


The view
Prev Chapter 17. Creating Application Functionality Next

The view

It is certainly possible to use Python and PyQt to write a custom editing component — you shoud probably base it on the QScrollView class. But making your own editor would entail a lot of very complicated work, mostly involved with datastructures to store text, text attributes, painting text and keeping track of the cursor position. And don't forget font handling, which gets complicated with Unicode. It would make quite an interesting project, but what's the use of a rich GUI library if you don't use it?

Therefore I propose to start out using the standard QMultiLineEdit widget. With PyQt for Qt 3.0, we can convert kalam to use the new editor widget, QTextEdit, which supports embedded pictures, hyperlinks and rich text. For now, we will have to be satisfied with plain text in a single font and a single color.

However, there is one problem with using a QMultiLineEdit editor widget as a view on the text: the widget itself contains a copy of the text. A QMultiLineEdit is a conflation of document and view. This means that we will have to synchronize the text in the document and in the view — recall that with our framework there can be more than one view on the same document. It is inevitable that we waste a lot of time copying text between views and documents. This shows that we should have implemented our own editor widget, one that is based on a separation of GUI and data.

The initial wrapping of QMultiLineEdit is pretty easy:

"""
kalamview.py - the editor view component for Kalam

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class KalamMultiLineEdit(QMultiLineEdit):

    def event(self, e):
        if e.type() == QEvent.KeyPress:
            QMultiLineEdit.keyPressEvent(self, e)
            return TRUE
        else:
            return QMultiLineEdit.event(self, e)

By default the QWidget's 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 type—and there are more than seventy event types in PyQt—is 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.

The view class encapsulates the editor widget we previously created:

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,
                     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 KDE's 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 setCaption(self, caption):
        QWidget.setCaption(self, caption)
        self.emit(PYSIGNAL("sigCaptionChanged"),
                  (self, caption))

    def document(self):
        return self.doc

    def closeEvent(self, e):
        pass

    def close(self, destroy=0):
        return QWidget.close(self, destroy)

    def changeDocument(self):
        if self._propagateChanges:
            self.doc.setText(self.editor.text(), self)

    def setText(self, text, view):
        if self != view:
            self._propagateChanges = FALSE
            self.editor.setText(text)
            self._propagateChanges = TRUE

The function changeDocument() is called whenever the textChanged() signal is emitted by the editor widget. Since we have a reference to the document in every view, we can call setText on the document directly. Note that we pass the document the changed text and a reference to this view.

The document again passes the view reference on when a sigDocTextChanged Python signal is emitted from the document. This signal is connected to all views that represent the document, and makes sure that the setText() function is called.

In the setText() function the view reference is used to check whether the changes originate from this view: if that is so, then it is nonsense to change the text. If this view is currently a 'slave' view — then the text of the QMultiLineEdit should be updated. Updating the text causes a textChanged() signal to be emitted — creating a recursion into oblivion.

To avoid the recursion, you can use the flag variable _propagateChanges. If this variable is set to FALSE, then the changeDocument() will not call the setText() function of the document.

Another solution would be to temporarily disconnect the textChanged() signal from the changeDocument() function. Theoretically, this would give a small performance benefit, since the signal no longer has to be routed nor the function called— but in practice, the difference is negligible. Connecting and disconnecting signal takes some time, too. Try the following alternative implementation of setText():

    def setText(self, text, view):
        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.

Prev | Home | Next
Creating Application Functionality | Up | The document


The document
Prev Chapter 17. Creating Application Functionality Next

The document

The KalamDocument document class is a simple wrapper around a QString object. The reason for using a QString to hold all text, instead of a Python string object, is straight-forward: with all the passing around of strings, converting the QStrings retrieved from the QMultiLineEdit widgets to Python strings would take far too much time. String conversion is a fairly costly operation, involving the copying of potentially large amounts of memory.

There is another reason for using QStringin this manner: a QString is mutable, and a Python string not. This means, simply put, that every time you change a single character in a Python string, a complete new copy of the string is made, and the old copy is discarded.

There are good reasons for this behavior. If Python strings were mutable, they could not be used to index Python dictionaries based on their character value, but only on the abstract object ID that all Python objects receive. Imagine the mess you would have if you changed the actual value of a key in a dictionary.

QString is a clever and optimized class. If two instances of QString have the same contents, there is a good chance that they will even share the memory needed to store the actual text. Furthermore, QString offers as rich a set of methods to mangle strings as Python does, so we don't 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 don't yet need is a excellent motto...

"""
kalamdoc.py - abstraction of a document with a certain encoding

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class KalamDoc(QObject):
    """
    The document represents a plain text with a certain encoding. Default
    is Unicode.

    signals: sigDocModified (boolean)
             sigDocTitleChanged (string)
             sigDocTextChanged (qstring, qwidget)

    """
    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()

Instead of wrapping a simple, silly boolean value, we now wrap s single QString object.

    def setText(self, text, view=None):
        self._text=text
        self._modified=TRUE
        self.emit(PYSIGNAL("sigDocTextChanged"),
                  (self._text, view))

Most of the above functions haven't changed the basic framework. Note that the slotModified function has disappeared. Modifying a text isn't 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.

Prev | Home | Next
The view | Up | Saving and loading documents


Saving and loading documents
Prev Chapter 17. Creating Application Functionality Next

Saving and loading documents

What's the use of an editor if it can't load and save texts? It would be of no use at all— and thus it is high time that we implemented this essential functionality. Loading and saving are part of the KalamDocument class. First, we need to decide if we will make use of the special PyQt file classes, or of the generic Python file classes. Let's do both for now, and you can choose which style you prefer.

Loading

Loading first:

    def open(self, fileName, format=None):
        self.setPathName(fileName)
        f = QFile(fileName)
        if f.exists():
            f.open(IO_ReadOnly)
            self.setText(QTextStream(f).read())
        else:
            raise IOError("No such file or directory: '%s'" % fileName)
        self._modified=FALSE

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 doesn't exist, an exception is raised, which is caught in the application class, KalamApp.

The Pythonic method is a lot shorter:

    def open(self, fileName, format=None):
        self.setPathName(fileName)
        self.setText(QString(open(str(fileName)).read()))
        self._modified=FALSE

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.

Saving

Saving text is slightly more critical than loading: what you can't load, you can't mangle and lose, but if the application refuses to save a text, a user can lose a lot of 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.

    def save(self, fileName = None, format = None):
        if fileName is not None and fileName <> "":
            self.setPathName(fileName)

        if self.pathName() == None:
            raise IOError("Could not save document: 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. Python's 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.

Prev | Home | Next
The document | Up | Undo, redo and other editing functions


Undo, redo and other editing functions
Prev Chapter 17. Creating Application Functionality Next

Undo, redo and other editing functions

The editing component we are using, QMultiLineEdit, already supports undo and redo using standard keys. Because undo and redo are defined as slots in the C++ source for QMultiLineEdit, they immediately affect the document and all views. The only thing left for us is to add these functions to the edit menu and the toolbar. The same principle holds for cut, copy, paste and select all.

The right place for these additions is the central application class, KalamApp. However, we need to do more than simply connect the correct QActions to the relevant slots in the view's editors. If we do that, then undo, for instance, would undo the last action in all views, simultaneously! We need to write special functions in KalamApp that work only on the active view, and we must wrap the QMultiLineEdit slots in the view component.

First the KalamView wrappings:

    def clear(self):
        self.editor.clear()

    def append(self, s):
        self.editor.append(s)

    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()

    def insert(self, s):
        self.editor.insert(s)

    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. I've used the GPL'ed 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)

        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"],
                     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, we'll have to distribute the actions by hand in the initMenu() and initToolbar() functions. Again, omitted code is elided with three dots (...).

    def initMenuBar(self):

        ...

        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):

        ...

        self.editToolbar = QToolBar(self, "edit operations")
        self.actions["editUndo"].addTo(self.editToolbar)
        self.actions["editRedo"].addTo(self.editToolbar)
        self.actions["editCut"].addTo(self.editToolbar)
        self.actions["editCopy"].addTo(self.editToolbar)
        self.actions["editPaste"].addTo(self.editToolbar)

        ...

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):
        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()

Prev | Home | Next
Saving and loading documents | Up | Conclusion


Conclusion
Prev Chapter 17. Creating Application Functionality Next

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.

Prev | Home | Next
Undo, redo and other editing functions | Up | Application Configuration


The Python way of handling configuration settings
Prev Chapter 18. Application Configuration Next

The Python way of handling configuration settings

Qt 3.0 offers a nice and integrated way of handling configuration settings. The Qt 3.0 way of working wasn't quite ready when I was writing this chapter, so I've just added a forward-looking statement in the last section of this chapter on how things will work Qt 3.0 is bound to Python.

Creating a configuration management framework is an interesting exercise in its own right, so, after exploring the standard modules Python offers, we'll build a simple framework that neatly fits kalam ourselves.

For now, the choice is between taking the easy way out, or conducting a really nice cross-platform solution. The easy way out is to store all settings in a settings file, both on Unix and on Windows. We can store this file in $HOME/.kalamrc, and prompt the Windows users to enter a setting for HOME in their control panel.

To store user settings the "right" way, the editor will have to determine if it is running on a Windows or Unix system. On Windows, the editor will store all configuration files in the registry (using the Python module _winreg), and on Linux in a dot file in the users home directory. We can structure the dot file with the Python ConfigParser module, which can read and write files of the old Windows .ini format.

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 foreseeable future we'll have Qt 3.0's QConfig, which will obsolete our own efforts.

If you want to keep your application firmly in the Python domain—perhaps with a view to later translate the application to another GUI toolkit—you 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.

Prev | Home | Next
Application Configuration | Up | Implementing configurations settings for Kalam


Implementing configurations settings for Kalam
Prev Chapter 18. Application Configuration Next

Implementing configurations settings for Kalam

Working with configuration settings can be divided into two main procedures: giving your application classes access to the configuration data, and loading and saving that data. We'll start by looking at the first problem, and then at loading and saving. In the next chapter, we'll round out Kalam by creating a preferences dialog.

Handling configuration data in your application

Before we start saving and restoring configuration settings, we should have a clear idea of how to handle them in the application. Configuration data typically must be 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 object—that 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

But also with a class:

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. It's 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 configuration module. An example would be a function to create a QFont out of a fontname string.

Well — that was the theory. Let's now look at the code needed to implement configuration data for Kalam. It's pretty similar to the snippets we saw above:

"""
kalamconfig.py - Configuration class for the Kalam Unicode Editor

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""

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,
                 Config.pointsize,
                 Config.weight,
                 Config.italic,
                 Config.encoding )

As you can see, it's 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.

Saving and loading the configuration data

Retrieving and saving the configuration data can be made as complex or easy as you want. We have already discussed the possibility of using _winreg or ConfigParser for the saving and retrieving of configuration data.

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:

def readConfig(configClass = Config):
    sys.stderr.write( "Initializing configuration\n")
    try:
        for line in open(os.path.join(os.environ["HOME"],
                                      Config.CONFIGFILE)).readlines():
            k, v=tuple(line.split("="))
            v=v[:-1]
            if v=="None\n":
                v=None
            elif type:
               try:
                    v=int(v)
                except ValueError:
                    pass
            setattr(configClass, k, v)
    except IOError:
        sys.stderr.write( "Creating first time configuration\n")

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()

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 object's hidden attributes. We retrieve these with the dir() function:

def writeConfig(configClass = Config):
    sys.stderr.write( "Saving configuration\n")
    configFile=open(os.path.join(os.environ["HOME"],".kalamrc"),"w+")
    for key in dir(Config):
        if key[:2]!='__':
            val=getattr(Config, key)
            if val==None or val=="None":
                line=str(key) + "=\n"
            else:
                line=str(key) + "=" + str(val) + "\n"
            configFile.write(line)
    configFile.flush()

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()

Using configuration data from the application

By now we have a simple configuration data mechanism, and it's time to use it. Earlier we defined a few settings: the position and size of the application window, the widget style that is to be used, and the interface paradigm. First, we will write some code to actually use these settings. Then we will write code to save changes when the application is closed.

Font settings

The font to be used in the editor window can be set and retrieved with the get and set functions we defined above. The KalamView class is the place to use this setting.

"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE

class KalamView(QWidget):

    def __init__(self, parent, doc, *args):
        apply(QWidget.__init__,(self, parent) + args)
        ...
        self.editor=QMultiLineEdit(self)
        self.editor.setFont(kalamconfig.getTextFont())
        self.layout.addWidget(self.editor)

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()).

Window geometry

Applying the geometry is just as easy. It's very pleasant for users when an application pops up its windows at the same place and in the same size as the user left them. This is part of session management, which is very advanced in the KDE environment, but less so for Windows. Qt 3 offers support for session management with QSessionManager and QApplication, but we'll take care of session management ourselves at this time.

Setting the correct size and position of a window, and also the correct widget style, is done in the central application object, KalamApp:

from qt import *
...

import kalamconfig
...

class KalamApp(QMainWindow):
    """KalamApp is the toplevel application window 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)

Here, too, we import the kalamconfig module. The function initSettings() is called from the constructor {__init__()}

This function will be extended with other application level settings during development of Kalam.

Determining the widget style

First, we set the desired widget style. Users can also set the widget style using a command-line option, and Qt can even figure out which style fits best with a users platform. But some people have strong preferences, and will want to configure their preferred style. It is easy enough to determine and use the platform default if no special style is set.

The getStyle() and setStyle are quite interesting, from a Python point of view:

def __extractStyle(style):
    if type(style) == InstanceType:
        return style.__class__.__name__
    elif type(style) == StringType:
        return style
    else:
        return "QPlatinumStyle"

I wanted this to be as flexible as possible, showing the dynamic nature of Python. The __extractStyle function takes the current style object that is used by the application. We find this by calling qApp.style(). qApp is a global variable that points to the QApplication object.

An instance in Python has a number of ‘hidden' fields and methods that each have a special meaning. One of these is __init__(), which is called when the object is first created. Another is __class__, which returns the class that the object was created from. You can use this to make more instances, but in this case we are interested in the string that contains the name of the class. You can retrieve the name with another ‘hidden' variable of the class class: __name__.

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 don't 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 shouldn't use eval for this, but it is a nice opportunity
        # for showing how it works. Normally you'd use a dictionary 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:

        styleDict = { "platinum": QPlatinumStyle, ...}

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)()

This means that, if the variable Config.currentStyle contains a string that is equal to classname and that is known to Python (that is, it can be found in one of the imported modules), eval() will return that class. The brackets after eval make an instance of the class.

Beware: using eval is dangerous. For example, what if someone hacked your .kalam-ch13 configuration file and set the entry currentStyle to os.rmdir('/')? If you were fool enough to run Kalam as root on Unix, you'd lose your system—irretrievably.

This is why I checked the existence and believability of the currentStyle string before eval-ing it. I only used eval to show you that it exists— for your own sake, don't use eval trivially! We'll return to eval and its friends in Chapter 20.

Setting the viewmanager

The last task we handle in this chapter is the choosing of the view manager. The available choices include tabbed windows, mini-windows, splitters, stacks — the lot. This time, we will use a dictionary that maps viewmanager names to actual classes. This is only to show you how it works - in general, it's a good rule to not mix and match approaches as we have done here, but to choose one method, and stick to it.

"""
kalamconfig.py - Configuration class for the Kalam Unicode Editor

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""

import sys, os, types
from qt import *

import tabmanager, listspace, splitspace, stackspace, workspace

workspacesDictionary = {
    "tabmanager" : tabmanager.TabManager,
    "listspace"  : listspace.ListSpace,
    "splitspace" : splitspace.SplitSpace,
    "stackspace" : stackspace.StackSpace,
    "workspace"  : workspace.WorkSpace,
    }

class Config:
...

First, a dictionary (workspacesDictionary) is created that contains a mapping from strings to the actual classes. Of course, in order to be able to access those classes, they will have to be imported.

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 doesn't 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
    ...

Catching the changes when the application closes

The configuration should be written to a file when the app closes. There are two places where Kalam can end: slotFileQuit(), and in the eventhandler eventFilter().

    ...
    #
    # 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()
                    kalamconfig.writeConfig()
                    event.accept()
                except Exception, e:
                    event.ignore()
        return QWidget.eventFilter(self, object, event)
     ...

After all, it is simply a matter of calling writeConfig() at the right moment.

Prev | Home | Next
The Python way of handling configuration settings | Up | Settings in Qt 3.0


Settings in Qt 3.0
Prev Chapter 18. Application Configuration Next

Settings in Qt 3.0

Qt 3.0 will have built-in cross-platform solutions for the management of configuration settings. Note that what I discuss here is based on the beta release of Qt 3.0, and thus is subject to change. It is also not a completely satisfactory solution, since you still need to be aware of the type of platform you're running on, whether Windows (all 32 bits flavors), Apple's OS X or any Unix with X11.

The Qt 3.0 system is built around the QSettings class. On Windows systems, all settings are saved in the registry; on Linux or other Unices settings are saved in a file. The exact location of the file is determined by a "search path". This is a list of directories, similar to the $PATH environment variable, that lists all places Qt will look for a configuration file.

QSettings saves all settings in a hierarchical tree format, conforming to the layout of the Windows registry. You cannot use keys longer than 255 Unicode characters or values longer than 16.300 characters (silly limitation, but there you are). The complete path—i.e., all keys plus the value—must fit into the memory of a Commodore 64 (that is, 64 kb). You can exceed these limits if you are targeting only Unix. It remains to be seen what the limitations will be on OS X, but since OS X is Unix based, you can assume it will follow the general Unix scheme of configuration files.

Let's translate the Kalam example above to QSettings. Note that this is untested code: I've merely extrapolated from the known C++ interface to what I assume will become the PyQt interface. (QSetting wasn't in the subset of Qt 3 classes that were implemented when I finished this text). Note also that there are two ways of 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

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""

import sys, os, types
from qt import *

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,
        "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")

def readConfig(configClass = Config):
    for key in configClass.defaults.keys():
        v = configClass.settings.readEntry("/kalam/" + key)
        configClass.key = v

def writeConfig(configClass = Config):
    sys.stderr.write( "Saving configuration\n")
    for key in dir(Config):
        if key[:2]!='__':
            val=getattr(Config, key)
            configClass.settings.writeEntry("/kalam/" + key, val)

...

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. That's also the directory where all Qt 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 user's 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.

Prev | Home | Next
Implementing configurations settings for Kalam | Up | Conclusion


Conclusion
Prev Chapter 18. Application Configuration Next

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!

Prev | Home | Next
Settings in Qt 3.0 | Up | Using Dialog Windows


Non-modal: Search and replace
Prev Chapter 19. Using Dialog Windows Next

Non-modal: Search and replace

In the previous section we constructed a deviously complex (at least, it felt that way) modal dialog box. Now we will attempt something comparable for a non-modal dialog box.

Design

What we are aiming for is a combined "search" and "search and replace" dialog box. It should conform to the following requirements:

  • Two kinds of search: plain text and regular expressions.

  • Search forward and backward

  • Case sensitive or insensitive search

  • Search from the beginning of the text or the current cursor position

  • Search on a selection or the whole text.

  • Choice between replace one occurrence after another, or or all occurrences.

A tall order? Certainly, but also quite probably very instructive. A few minutes with the Designer gives us the following, esthetically pleasing, dialog box:

The find and replace dialog.

Integration in the application

Implementing all this functionality is quite complex, so it is best to first make sure that we can call the find and replace dialog window from the application. This entails adding two QAction's to the action dictionary, an icon to resources.py, and two new slots—and creating the dialog, of course.

You don't create, run and destroy a non-modal dialog, like we did with the settings dialog. Instead, you create it once, and show it whenever necessary. The Close button on the dialog doesn't really close it; it merely hides the window. In this case, the find and replace dialog is created in the constructor of KalamApp:

...
from dlgfindreplace import DlgFindReplace
...
class KalamApp(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__,(self, ) + args)
        ...
        # Create the non-modal dialogs
        self.dlgFindReplace = DlgFindReplace(self, "Find and replace")

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
        #

        ...
        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 isn't 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)

Because the combined find/find and replace dialog has two modes, it is necessary to have two ways of calling it—one 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, let's 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())

The actual implementation in DlgFindReplace of these show function is quite simple. The Find option hides certain widgets, after which the automatic layout management ensures that the dialog looks as good as it should. The Find and Replace options makes sure they are shown. The window caption is adapted, too. Note that you must first call show() on the entire dialog, and only then show() on the previously hidden widgets, otherwise the layout manager doesn't show the appearing widgets.

    def showFind(self, document, view):
        FrmFindReplace.show(self)
        self.setCaption("Find in " + document.title())
        self.bnReplaceNext.hide()
        self.bnReplaceAll.hide()
        self.grpReplace.hide()
        self.initOptions(document, view)

    def showReplace(self, document, view):
        FrmFindReplace.show(self)
        self.setCaption("Find and replace in " + document.title())
        self.bnReplaceNext.show()
        self.bnReplaceAll.show()
        self.grpReplace.show()
        self.initOptions(document, view)

The result is pretty enough to show:

The Find dialog.

Implementation of the functionality

Now that we can show the find and replace dialog, it is time to implement some functionality. Again, we subclass the generated design and add what we need.

"""
dlgfindreplace.py - Findreplace dialog for Kalam.

See: frmfindreplace.ui

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
import os, sys
from qt import *

import kalamconfig

from resources import TRUE, FALSE
from frmfindreplace import FrmFindReplace

class DlgFindReplace(FrmFindReplace):
    """ A full-featured search and replace dialog.
    """
    def __init__(self,
                 parent = None,
                 name = None):
        FrmFindReplace.__init__(self, parent, 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)
        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.

    def showFind(self, document, view):
        FrmFindReplace.show(self)
        self.bnFind.setDefault(TRUE)
        self.setCaption("Find in " + document.title())
        self.bnReplaceNext.hide()
        self.bnReplaceAll.hide()
        self.grpReplace.hide()
        self.cmbFind.setFocus()
        self.init(document, view)

    def showReplace(self, document, view):
        FrmFindReplace.show(self)
        self.setCaption("Find and replace in " + document.title())
        self.bnReplaceNext.show()
        self.bnReplaceNext.setDefault(TRUE)
        self.bnReplaceAll.show()
        self.grpReplace.show()
        self.cmbFind.setFocus()
        self.init(document, view)

As we discussed above, there are two show functions (showFind() and showReplace), each hides or shows the widgets that are relevant. The show functions also call the initialization function init().

    def init(self, document, view):
        self.document = document
        self.view = view

        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 forward/backward checkboxes whenever the user selects the regular expressions search mode, and code to make forward searching the default.

On regular expressions: It is quite probable that you know more about regular expressions than I do. I can't write them for toffee, and I find reading regular expressions to be even harder (despite the fact that I used to be a dab hand at Snobol). Nonetheless, regular expressions are indispensable when searching a text. Even I know how to use $ to specify the end of input or \n to specify a new line. Regular expressions are everywhere on a Unix system, and all decent editors (as well as Python, Perl and most other languages) support them. On Windows, you can enter regular expressions in the search function of Word (or so I am told).

A regular expression is nothing more than an algebraic notation for characterizing a set of strings. An expression represents a pattern that the regular expression engine can use to match text. Python comes with its own highly capable, high performance regular expression engine, compared with which the regular expression engine in Qt 2.3 is a bit puny. The regular expression engine of Qt 3.0 has been improved a lot, and is nearly as good as the Python one.

According to the Qt online documentation, the Qt 2.3 QRegExp class recognized the following primitives:

  • c - the character 'c'

  • . - any character (but only one)

  • ^ - matches start of input

  • $ - matches end of input

  • [] - matches a defined set of characters. For instance, [a-z] matches all lowercase ASCII characters. Note that you can give a range with a dash (-) and negate a set with a caron (^ - [^ab] match anything that does contain neither a nor b)/

  • c* - matches a sequence of zero or more character c's

  • c+ - matches a sequence of one or more character c's

  • c? - matches an optional character c

  • \c - escape code for special characters such as \, [, *, +, . etc.

  • \t - matches the TAB character (9)

  • \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)

  • \s - matches a white space (defined as any character for which QChar::isSpace() returns TRUE. This includes at least ASCII characters 9 (TAB), 10 (LF), 11 (VT), 12(FF), 13 (CR) and 32 (Space)).

  • \d - matches a digit (defined as any character for which QChar::isDigit() returns TRUE. This includes at least ASCII characters '0'-'9').

  • \x1f6b - matches the character with unicode point U1f6b (hexadecimal 1f6b). \x0012 will match the ASCII/Latin1 character 0x12 (18 decimal, 12 hexadecimal).

  • \022 - matches the ASCII/Latin1 character 022 (18 decimal, 22 octal).

Being constitutionally unable to explain exactly how you go about creating regular expressions that work, I can only refer you to the online documentation of Python and Qt, and to the many tutorials available on the web. Qt 3.0 comes with an excellent page on regular expressions, too. Whenever I read Part I of Jurafsky and Martin's book ‘Speech and Language Processing', I have the feeling that I understand regular expressions, and I have never found that to be the case with any other text on the subject.

As a last note: both Python and Qt regular expressions work just fine with Unicode. Back to our code...

    def slotCaseSensitive(self):
        pass

    def slotBeginning(self):
        self.setFindExtent()

    def slotSelection(self):
        self.setFindExtent()

    def slotForward(self):
        self.setFindExtent()

    def slotBackward(self):
        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():
                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",
                                   "Begin reached. Start from end?",
                                   "yes",
                                   "no",
                                   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 Python string, so we use QRegExp here (though it is a little underpowered in its Qt 2.3 incarnation compared to re and Qt 3.0's QRegExp).

A QRegExp is constructed from a string that contains the patterns that should be matched, and two options. The first option determines whether the search should be case sensitive; the second determines whether or not the search is a wildcard search. Wildcard searches work like the filename expansion on a Unix command line, and are not terribly useful for searching in a text.

QRegExp has two tools for searching: match() and find(). Both take as parameters the QString to be searched and the position from which searching should start. However, match() also returns the length of the string that is found, and can take an optional parameter that indicates whether the start position should match the regular expression character "^" (start of input). You don't want this for searching in editable text, so we make it FALSE by default.

Literal searching is a simple matter of applying the find() method of QString from the current position.

Looking for an occurrence returns either -1, if nothing was found, or the begin and end positions of the string that was found. Note that QString.find() doesn't return the length of the found string; we take the length() of the search string to determine the end position.

    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 doesn't 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:
                self.view.setSelection(begin,
                                       end)
                self.currentPosition = end
                return (begin, end)
            else:
                if (self.chkSelection.isChecked() == FALSE and
                    self.chkWholeText.isChecked() == FALSE):
                    self.wrapExtentForward()
                return (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()
                return (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()
        while begin > -1:
            begin, end = self.slotReplaceNext()
            print begin, end

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 editor's 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 don't emit the textChanged() signal— you will have to do that yourself. If we do not emit textChanged(), other views on the same document won't 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):
        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 setCursorPosition(self, p):
        """Sets the cursor of the editor at position p in the text."""
        l, c = self.getLineCol(p)
        self.editor.setCursorPosition(l, c)

    def getPosition(self, startline, startcol):
        if startline = 0:
            return startcol
        if startline > self.editor.numLines():
            return self.editor.text().length()
        i=0
        for line in range(self.editor.numLines()):
            if line < startline:
                i += self.editor.textLine(line).length()
            else:
                return i + startcol

    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)

The function getLineCol() takes a single index position as argument. It then loops through the lines of the editor, subtracting the length of each line from a 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 can't get at QTextDocument (it's not even documented, you need to read the Qt source code to find out about it), so it's 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 programmer's 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.

Prev | Home | Next
Using Dialog Windows | Up | Conclusion


Conclusion
Prev Chapter 19. Using Dialog Windows Next

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.

Prev | Home | Next
Non-modal: Search and replace | Up | A Macro Language for Kalam


Integrating macros with a GUI
Prev Chapter 20. A Macro Language for Kalam Next

Integrating macros with a GUI

Before we can start defining what we allow our users to do to Kalam, we need to build the core macro execution functionality. The first step is to make sure users can execute the contents of a document.

Executing the contents of a document

Unless you have skipped all previous occasions of creating and adding an action to the menubar and the toolbar, you will know by now how to do so. I have supplied the slot code that executes the contents of the currently active document. You will find the complete code in the kalamapp.py file that belongs to this chapter.

# kalamapp.py
...
class KalamApp(QMainWindow):
...
    # Macro slots
...
    def slotMacroExecuteDocument(self):
        if self.docManager.activeDocument() == None:
            QMessageBox.critical(self,
                                 "Kalam",
                                 "No document to execute as a macro ")
            return

        try:
            bytecode = compile(str(self.docManager.activeDocument().text()),
                               "<string>",
                               "exec")
        except Exception, e:
            QMessageBox.critical(self,
                                 "Kalam",
                                 "Could not compile " +
                                 self.docManager.activeDocument().title() +
                                 "\n" + str(e))
        try:
            exec bytecode # Note: we don't yet have a separate 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 output—that 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))

Having done that, redirecting all output is easy:

        ...
    def slotMacroExecuteDocument(self):
        ...
            import sys
            document = self.docManager.createDocument(KalamDoc, KalamView)
            document.setTitle("Output of " + title)
            oldstdout = sys.stdout
            oldstderr = sys.stderr

            sys.stdout = document
            sys.stderr = document

            exec bytecode # Note: we don't yet have a separate namespace
                          # for macro execution

            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.

Executing a bit of code from a document.

Of course, littering the KalamApp with macro execution code isn't 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 won't show the unit tests here: it is available with the full source code.

"""
macromanager.py - manager class for macro administration and execution

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""

from qt import *
import sys

class MacroError(Exception):pass

class NoSuchMacroError(MacroError):

    def __init__(self, macro):
        ERR = "Macro %s is not installed"
        self.errorMessage = ERR % (macro)

    def __repr__(self):
        return self.errorMessage

    def __str__(self):
        return self.errorMessage

class CompilationError(MacroError):

    def __init__(self, macro, error):
        ERR = "Macro %s could not be compiled. Reason: %s"
        self.errorMessage = ERR % (macro, str(error))
        self.compilationError = error

    def __repr__(self):
        return self.errorMessage

    def __str__(self):
        return self.errorMessage

class ExecutionError(MacroError):

    def __init__(self, error):
        ERR = "Macro could not be executed. Reason: %s"
        self.errorMessage = ERR % (str(error))
        self.executionError = error

    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):

    def __init__(self, code, *args):
        apply(QAction.__init__,(self,) + args)
        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 addTo(self, widget):
        apply(QAction.addTo,(self, widget))
        self.locations.append(widget)

    def removeFrom(self, widget):
        QAction.removeFrom(self, widget)
        del self.locations[widget]

    def remove(self):
        for widget in self.locations:
            self.removeFrom(widget)

    def __compile(self, code):
        try:
            bytecode = compile(code,
                               "<string>",
                               "exec")
            return bytecode
        except Exception, e:
            raise CompilationError(macroName, e)

    def execute(self, out, err, globals, locals):
        try:
            oldstdout = sys.stdout
            oldstderr = sys.stderr

            sys.stdout = out
            sys.stderr = err
            exec self.bytecode in globals
            sys.stdout = oldstdout
            sys.stderr = oldstderr
        except Exception, e:
            print e
            print sys.exc_info
            sys.stdout = oldstdout
            sys.stderr = oldstderr
            raise ExecutionError(e)

By encapsulating each macro in a QAction, it will become very easy to assign shortcut keys, menu items and toolbar buttons to a macro.

The MacroAction class also takes care of compilation and execution. The environment, consisting of the globals and locals dictionaries, is passed in the execute() function. We also pass two objects that replace the standard output and standard error objects. These can be Kalam documents, for instance. Note how we carefully restore the standard output and standard error channels. The output of the print statement in the exception clause will go to the redefined channel (in this instance, the kalam document).

class MacroManager(QObject):

    def __init__(self, parent = None, g = None, l = None, *args):
        """ Creates an instance of the MacroManager.
        Arguments:
        g = dictionary that will be used for the global namespace
        l = dictionary that will be used for the local namespace
        """
        apply(QObject.__init__,(self, parent,) + args)

        self.macroObjects = {}

        if g == None:
            self.globals = globals()
        else:
            self.globals = g

        if l == None:
            self.locals = locals()
        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).

    def deleteMacro(self, macroName):
        del self.macroObjects[macroName]

    def addMacro(self, macroName, macroString):
        action = MacroAction(macroString, self.parent())
        self.macroObjects[macroName] = action
        self.connect(action,
                     PYSIGNAL("activated"),
                     self.executeAction)
        return action

    def executeAction(self, action):
        action.execute(sys.stdout,
                       sys.stderr,
                       self.globals,
                       self.locals)

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()
        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 execute as a macro ")
            return

        title = self.docManager.activeDocument().title()

        try:
            macroText = 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 execution code.")
        except ExecutionError, e:
            QMessageBox.critical(self,
                                 "Kalam",
                                 "Error executing " + title +
                                 "\n" + str(e))
        except Exception, e:
            QMessageBox.critical(self,
                                 "Kalam",
                                 "Unpleasant error %s when trying to run %s." \
                                 % (str(e), title))

Note the careful handling of exceptions. You don't want your application to crash or become unstable because of a silly error in a macro.

startup macros

Executing the contents of a document is very powerful in itself—especially since we have access to the complete KalamApp object, from which we can reach the most outlying reaches of Kalam.

It would be very unpleasant for a user to have to load his macros as a document every time he wants to execute a macro. Ideally, a user should be able to define a set of macros that run at start-up, and be able to add macros to menu options and the keyboard.

Solving the first problem takes care of many other problems in one go. Users who are capable of creating macros are probably able to create a startup macro script that loads all their favorite macros.

We define two keys in the configuration file, macrodir and startupscript. These are the name and location of the Python script that is executed when Kalam is started. We start a user macro after all standard initialization is complete:

# kalamapp.py - fragment
...
class KalamApp(QMainWindow):
    def __init__(self, *args):
        apply(QMainWindow.__init__,(self, ) + args)
        ...
        # Run the startup macro script
        self.runStartup()

    ...

    def runStartup(self):
        """Run a Python script using the macro manager. Which script is
        run is defined in the configuration variables macrodir and startup.

        All output, and eventual failures are shown on the command-line.
        """
        try:
            startupScript = os.path.join(kalamconfig.get("macrodir"),
                                         kalamconfig.get("startupscript"))
            startup = open(startupScript).read()
            self.macroManager.addMacro("startup", startup)
            self.macroManager.executeMacro("startup")
        except Exception, e:
            print "Could not execute startup macro", e

A sample startup script might start Kalam with an empty document:

# startup.py - startup script for Kalam
print "Kalam startup macro file"
self.docManager.createDocument(KalamDoc, KalamView)

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.

Prev | Home | Next
A Macro Language for Kalam | Up | Creating a macro API from an application


Creating a macro API from an application
Prev Chapter 20. A Macro Language for Kalam Next

Creating a macro API from an application

Enabling users to execute any bit of Python code they might have lying around, from a document, the menu or keyboard, isn't enough to macro-enable Kalam. For this, we must offer a clean and clear set of functions that can be used to manipulate the data and interface of our application. This is the hardest part, actually. If you can't accomplish this, you might as well tell users to hack your application directly.

One problem is already apparent in the startup script we created in the previous section. The macro writer needs to call a nebulous entity named self, but how is he to know that this is a reference to the application itself?

It would be more effective to allow access to the application using a logical name. The solution is to add an extra entry to the namespace that is used to initialize the macro manager. This is as simple as adding a key to a dictionary. Let's revisit the initMacroManager() function and add the current KalamApp object:

    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())

By declaring a variable global, it becomes part of the global namespace. Passing that namespace to exec gives all executed code access to the variable.

Accessing the application itself

As an example, I have created a few functions that simplify the creation of new documents and macro's. Because a macro is wrapped in MacroAction, which is a subclass of QAction, it's very easy to add them to the menu.

    # kalamapp.py
    #
    # Macro API
    #
    def installMacro(self,
                     action,
                     menubar = None,
                     toolbar = None):
        """
        Installs a certain macro action in the menu and/or the toolbar
        """
        if menubar != None:
            action.addTo(menubar)
        if toolbar != None:
            action.addTo(toolbar)

    def removeMacro(self, action):
        action.remove()

    def createDocument(self):
        doc, view = self.docManager.createDocument(KalamDoc, KalamView)
        return (doc, view)

    def createMacro(self, name, code):
        return self.macroManager.addMacro(name, code)

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
        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. Here's 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.

Accessing application data

This application collects its data in the KalamDoc object and shows it using the KalamView object. By giving a user access to the entire internal object model via the DocManager object, we make it possible to script the creation, modification and saving of documents.

Accessing and extending the GUI

The function installMacro, which we have already seen, is used to add a macro to any menubar or toolbar. What text the macro shows, what tooltips, what icon and what accelerator key is all determined by the settings of the underlying QAction object. In the example above, we didn't set a shortcut or an icon.

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, let's not be so serious and sensible, and implement something a little more frivolous.

Kalam rivals Emacs: an Eliza macro

One of the first extensions to the Emacs editor, way back in the beginning of the eighties, was an Eliza application. A kind of Rogerian psychoanalyst that took the user's input, analyzed it a bit, and said something comprehensible back.

This is actually a very nice example of working with documents in an editor, since the macro must be aware (more or less) of what the user typed in, and be able react to the pressing of the Enter key. Surely having all the power of Python at our disposal means that we can at least achieve equal status with the doyen of editors!

So, without further ado, I present Edmund - who doesn't really listen, but does answer back, in his accustomed vein:

import random

class Edmund(QObject):
    """
    An Edmund macro for the Kalam Editor.

    Of course, if I really re-implemented Eliza, the responses would bear
    some relevance to the input. Anyway.
    """

    def __init__(self, *args):
        QObject.__init__(self)
        self.responses = [
            "First Name?",
            "Come on, you MUST have a first name.",
            "Sod Off?",
            "Madam, without you, life was like a broken pencil...pointless.",
            "So what you are saying, Percy, is something you have never" +
            " seen is slightly less blue than something else . . that you " +
            "have never seen?",
            "I'm afraid that might not be far enough. " +
            "Apparently the head Mongol and the Duke are good friends. " +
            "They were at Eton together.",
            "Ah ah, not so fast! Not that it would make any difference. " +
            "We have the preliminary sketches...",
            "You have absolutely no idea what irony is, have you Baldrick?",
            "Baldric, you wouldn't recognize a subtle plan if it painted  " +
            "itself purple and danced naked on a harpsichord singing " +
            "'subtle plans are here again'.",
            "Baldric, you have the intellectual capacity of a dirty potato.",
            "Ah, yes. A maternally crazed gorilla would come in handy " +
            "at this very moment.",
            "That would be as hard as finding a piece of hay in an " +
            "incredibly large stack of needles.",
            "Normal procedure, Lieutenant, is to jump 200 feet in the air " +
            "and scatter oneself over a wide area.",
            "I think I'll write my tombstone - Here lies Edmund Blackadder" +
            ", and he's bloody annoyed.",
            "As a reward, Baldrick, take a short holiday. " +
            ".... Did you enjoy it?"
    ]

        self.doc, self.view = createDocument()
        self.doc.setTitle("Talk to Edmund BlackAdder")
        self.connect(self.view,
                     PYSIGNAL("returnPressed"),
                     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 API's 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.

Talking heart to heart with your computer.

Prev | Home | Next
Integrating macros with a GUI | Up | Conclusion


Conclusion
Prev Chapter 20. A Macro Language for Kalam Next

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.

Prev | Home | Next
Creating a macro API from an application | Up | Drawing on Painters and Canvases


QCanvas
Prev Chapter 21. Drawing on Painters and Canvases Next

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.

the relation between QCanvas, QCanvasItems and QCanvasView

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.

Even QCanvasPolygonalItem itself is not terribly useful: the derived classes QCanvasEllipse, QCanvasLine, QCanvasPolygon and QCanvasRectangle can be used to draw ellipses, lines, polygons and rectangles on the canvas. Interestingly, these items can have a non-square bounding box.

This means that two circles won't touch if they overlap the box that contains them: only if the circles themselves touch. This is quite special, and if you create new derivations of these classes yourself, you should take care to carefully calculate the area your object occupies.

Overlapping and non-overlapping circles.

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 you'll 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.

A simple Unicode character picker

The goal of this example is to provide a point-and-click way of entering characters 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.

A Unicode character picker

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:

# Start Code; End Code; Block Name
0000; 007F; Basic Latin
0080; 00FF; Latin-1 Supplement
0100; 017F; Latin Extended-A
0180; 024F; Latin Extended-B
0250; 02AF; IPA Extensions
02B0; 02FF; Spacing Modifier Letters
0300; 036F; Combining Diacritical Marks
...
F900; FAFF; CJK Compatibility Ideographs
FB00; FB4F; Alphabetic Presentation Forms
FB50; FDFF; Arabic Presentation Forms-A
FE20; FE2F; Combining Half Marks
FE30; FE4F; CJK Compatibility Forms
FE50; FE6F; Small Form Variants
FE70; FEFE; Arabic Presentation Forms-B
FEFF; FEFF; Specials
FF00; FFEF; Halfwidth and Fullwidth Forms
FFF0; FFFD; Specials

This file can be used to fill a combobox with all different scripts:

Example 21-2. charmap.py - a Unicode character selection widget

"""
charmap.py - A unicode character selector

copyright: (C) 2001, Boudewijn Rempt
email:         boud@rempt.xs4all.nl
"""

import string, os.path
from qt import *

TRUE=1
FALSE=0

class CharsetSelector(QComboBox):

    def __init__(self, datadir, *args):
        apply(QComboBox.__init__,(self,)+args)
        self.charsets=[]
        self.connect(self,
                     SIGNAL("activated(int)"),
                     self.sigActivated)

        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

    def sigActivated(self, index):
        begin, start=self.charsets[index]
        self.emit(PYSIGNAL("sigActivated"),(begin, start))

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 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.

The canvas

Working with QCanvas entails handling two classes: QCanvas and QCanvasView. In this section, we'll lay out the Unicode table on the QCanvas. From PyQt 3.0 onwards, the canvas classes are in a separate module: qtcanvas, which has to be imported separately.

You can think of a QCanvas as a virtually boundless two-dimensional paste-board, which you can fill with QCanvasItems. The main difference between a QCanvas with QCanvasItems on it and a QWidget with a lot of sub-widgets, is that the first is a lot more efficient in terms of memory-use, and offers easy collision detection. Of course, QCanvasItems are not widgets, so you don't have easy event handling — but you can fake it easily enough, by catching mouse presses on individual QCanvasItems.

Here, we will create a QCanvasText for every Unicode glyph. In the QCanvasView mouse-clicks on those items will be caught.

class CharsetCanvas(QCanvas):

    def __init__(self, parent, font, start, end, maxW, *args):
        apply(QCanvas.__init__,(self, ) + args)
        self.parent=parent
        self.start=start
        self.end=end
        self.font=font
        self.drawTable(maxW)

    def drawTable(self, maxW):
        self.maxW=maxW
        self.items=[]
        x=0
        y=0

        fontMetrics=QFontMetrics(self.font)
        cell_width=fontMetrics.maxWidth() + 3
        if self.maxW < 16 * cell_width:
            self.maxW = 16 * cell_width
        cell_height=fontMetrics.lineSpacing()

        for wch in range(self.start, self.end + 1):
            item=QCanvasText(QString(QChar(wch)),self)
            item.setFont(self.font)

            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

        if self.parent.height() > y + cell_height:
            h = self.parent.height()
        else:
            h = y + cell_height

        self.resize(self.maxW + 20, h)
        self.update()

    def setFont(self, font):
        self.font=font
        self.drawTable(self.maxW)

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.

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 won't show unless you resize the canvas to include them.

Finally, you must update() the QCanvas — otherwise you still won't 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 I'm 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...

The view on the canvas

Putting stuff on a canvas is useless, unless you can also see what you've done. You can create one or more QCanvasView objects that show the contents of canvas. Each view can show a different part, but every time you call update() (or advance(), which advances all animated objects), all views are updated.

The most important work your QCanvasView subclasses have is to react on user input. Here, we draw a cursor rectangle round selected glyphs and emit signals for every mousepress.

class CharsetBrowser(QCanvasView):

    def __init__(self, *args):
        apply(QCanvasView.__init__,(self,)+args)

    def setCursor(self, item):
        self.cursorItem=QCanvasRectangle(self.canvas())
        self.cursorItem.setX(item.boundingRect().x() -2)
        self.cursorItem.setY(item.boundingRect().y() -2)
        self.cursorItem.setSize(item.boundingRect().width() + 4,
                                item.boundingRect().height() + 4)

        self.cursorItem.setZ(-1.0)
        self.cursorItem.setPen(QPen(QColor(Qt.gray), 2, Qt.DashLine))
        self.cursorItem.show()
        self.canvas().update()

    def contentsMousePressEvent(self, ev):
        try:
            items=self.canvas().collisions(ev.pos())
            self.setCursor(items[0])
            self.emit(PYSIGNAL("sigMousePressedOn"), (items[0].text(),))
        except IndexError:
            pass

    def setFont(self, font):
        self.font=font
        self.canvas().setFont(self.font)

First, the drawing of the cursor. You can see that you don't 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. It's 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 PyQt's documentation (or the C++ Qt documentation) on QCanvasView, you will notice that it is not very well endowed with useful functions. QCanvasView is a type of specialized QScrollView, and this class offers lots of useful methods (for example, event handling methods for mouse events).

One of these methods, contentsMousePressEvent, is highly useful. It is called whenever a user clicks somewhere on the canvas view. You can then use the coordinates of the click to determine which QCanvasItem objects were hit. The coordinates of the mouse click can be retrieved with the pos() function of the ev QMouseEvent. You then check which QCanvasItem objects were hit using the collision detection QCanvas provides with the collisions().

The result is a list of items. Because we know that there are no overlapping items on our canvas, we can simply take the first QCanvasText item: that's items[0]. Now we have the selected glyph. The setCursor() function is called to draw a rectangle around the glyph. Then a signal is emitted, which can be caught by other widgets. This signal is ultimately responsible for getting the selected character in the Kalam document.

Tying the canvas and view together

The CharMap widget is a specialized QWidget that contains the three components we developed above.

A vertical layout manager contains the selection combobox and the CharsetBrowser QCanvasView widget. Every time a new script is selected, a new CharsetCanvas is created — this is easier than erasing the contents of the existing canvas.

class CharMap(QWidget):
    def __init__(self,
                 parent,
                 initialFont = "arial",
                 datadir = "unidata",
                 *args):

        apply(QWidget.__init__, (self, parent, ) + args)
        self.parent=parent
        self.font=initialFont
        self.box=QVBoxLayout(self)
        self.comboCharset=CharsetSelector(datadir, FALSE, self)
        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.setCaption("Unicode Character Picker")

        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.

    def setFont(self, font):
        self.font=font
        self.charsetBrowser.setFont(font)

    def sigCharacterSelected(self, text):
        self.emit(PYSIGNAL("sigCharacterSelected"), (text,))

Every time a user selects a character, the sigCharacterSelected signal is emitted. In KalamApp, this signal is connected to the a slot function that inserts the character in the current view or window.

    def slotShowCharset(self, begin, end):
        self.setCursor(Qt.waitCursor)

        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 cursor—this 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.

Saving Unicode files

Recall Chapter 8 on Unicode— if you implement this character map and want to save your carefully created Thai letter, you will be greeted by an encoding error.

To avoid that, you need to use of the unicode function instead of str() when converting the KalamDoc.text QString variable to Python strings.

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 layouts for alphabetic scripts, like Cyrillic or Thai, and input method editors for languages like Chinese.

Properly speaking, it's 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, it's 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")}

            def keyPressEvent(self, ev):
                if keymap.has_key(ev.key()):
                    self.insert(keymap[ev.key()])
                else:
                    QMultiLineEdit.keyPressEvent(self, ev)

Input method editors (IME's) are more difficult. Installing the free Chinese or Japanese IME's 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:

A Chinese input method editor written in Python and PyQt.

You can find the code for a stand-alone Pinyin-based Chinese IME at http://www.valdyas.org/python/qt2.html — it's 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).

Prev | Home | Next
Drawing on Painters and Canvases | Up | Conclusion


Conclusion
Prev Chapter 21. Drawing on Painters and Canvases Next

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 can't help you with that task — I've never worked with three-dimensional graphics myself, and besides, my computer isn't up to it.

Prev | Home | Next
QCanvas | Up | Gui Design in the Baroque Age


Faking it with bitmaps
Prev Chapter 22. Gui Design in the Baroque Age Next

Faking it with bitmaps

The first stab at a totally cool gui will be an imitation of a remote control. This will involve a nice brushed steel background with rounded buttons and a deep bevel. The remote control used in this instance is part of a system for medical imagery, and we want to duplicate the actual control on screen so that users familiar with the hardware won't have to learn a new interface for use with the software.

To create the interface, I first scanned a picture of the actual remote control unit. This gives us the picture shown below:

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.

Changing the shadows with the Gimp

This part of the proceedings is a bit of a bore: let's skip drawing a few of those buttons, and start with the implementation. A real remote control doesn't have window borders, so ours shouldn't 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 don't want borders.

Example 22-1. remote.py - remote control application

#
# remote.py - remote control application
#

import os, sys
from qt import *

from view     import *

class Window(QMainWindow):

  def __init__(self, *args):
    QMainWindow.__init__(self, None, 'RemoteControl',
            Qt.WStyle_Customize | Qt.WStyle_NoBorderEx)
    self.initView()

  def initView(self):
    self.view = View(self, "RemoteControl")
    self.setCentralWidget(self.view)
    self.setCaption("Application")
    self.view.setFocus()

def main(argv):
  app=QApplication(sys.argv)
  window=Window()
  window.show()
  app.connect(app, SIGNAL('lastWindowClosed()'), app, SLOT('quit()'))
  app.exec_loop()

if __name__=="__main__":
  main(sys.argv)

Next, we create the main view:

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)]

  def __init__(self, *args):
    apply(QWidget.__init__,(self,)+args)
    self.setFixedSize(245,794)
    self.setBackgroundPixmap(QPixmap("remote.gif"))
    self.buttons=[]
    for bndef in self.buttondefs:
      print bndef
      bn=Button(self, bndef[0], bndef[1], bndef[2], bndef[0]+'up.gif', bndef[0]+'down.gif')
      self.buttons.append(bn)
      QObject.connect(bn, PYSIGNAL("pressed"), self.Pressed)

  def Pressed(self, name):
    print "Button", name, "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())

  def mousePressEvent(self, ev):
    self.setBackgroundPixmap(self.pxDown)

  def mouseReleaseEvent(self, ev):
    self.setBackgroundPixmap(self.pxUp)
    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. (It's extremely tedious, I'm 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.

Prev | Home | Next
Gui Design in the Baroque Age | Up | Creating themes with QStyle


Creating themes with QStyle
Prev Chapter 22. Gui Design in the Baroque Age Next

Creating themes with QStyle

We've just created a beautifully sculpted interface, with all the highlights exactly right. Of course, the process lacked any flexibility. Fortunately, PyQt offers complete access to the Qt theming mechanism. Using this mechanism we can achieve almost the same effect, but in a more flexible and generic manner.

The key class here is QStyle. This class provides an enormous number of methods that are used by Qt's widgets to express themselves on screen — and a few hooks that you can use to intervene just before a widget paints itself. QCommonStyle is based on QStyle, and implements most of the actual drawing routines. On top of QCommonStyle is a whole tree of styles for Motif, Windows, MacOS-platinum, SGI and CDE. A desktop environment like KDE adds a whole host of other styles - and we can add one, too, once we have designed it.

Designing the style

Designing a successful widget style is an artistic endeavor that calls for sensitive handling and an acute awareness of usability principles, as can easily be seen from the thousands of horrible styles available from websites like www.themes.org. But then, designing a user interface calls for the same expertise. That's the reason most software houses employ interaction designers and graphic artists. Software developers who don't 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, I've 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, we'll 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.

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. This is shown in the themes.py example in your BlackAdder or PyQt distribution. Alternatively, you can write your own drawing routines. It is surprising that Python is fast enough to allow re-implementing the most basic drawing routines.

A Qt 2 custom style

Example 22-4. A Qt 2 custom style - a minimalist implementation of the classic Mac style in PyQt.

#
# 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)

    def polish(self, object):

        # Overloading doesn't work here, for Python cannot distinguish
        # between QApplication and QWidget - Python can only overload based
        # on the number of arguments.

        if isinstance(object, QApplication):
            self.polish_qapplication(object)
        elif isinstance(object, QWidget):
            self.polish_qwidget(object)
        else:
            QPlatinumStyle.polish(self, object)

    def unPolish(self, object):
        if isinstance(object, QApplication):
            self.unPolish_qapplication(object)
        elif isinstance(object, QWidget):
            self.unPolish_qwidget(object)
        else:
            QPlatinumStyle.unPolish(self, object)

    def polish_qapplication(self, app):
        # Set a font that's an approximation of Chicago: keep a ref to
        # the old font
        self.oldFont=app.font()
        app.setFont(QFont("chicago",
                            app.font().pointSize()-2, QFont.Bold),
                            TRUE)

        # keep a reference to the old color palette, otherwise
        # it cannot be restored
        self.oldPalette=app.palette()
        # create a new palette - black, white and 50% gray for text and buttons

        # 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
        active.setColor(QColorGroup.Foreground,
                        black) # general foreground color
        active.setColor(QColorGroup.Base,
                        white) # lighter background 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)
        disabled.setColor(QColorGroup.Text, darkgray)

        inactive=QColorGroup(active)

        inactive.setColor(QColorGroup.Text, darkgray)
        self.newPalette=QPalette(active, disabled, inactive)

        app.setPalette(self.newPalette, TRUE)

    def unPolish_qapplication(self, app):
        # Restore the old palette
        app.setFont(self.oldFont,  TRUE)
        app.setPalette(self.oldPalette, TRUE)

    def polish_qwidget(self, w):
        # Hook to set attributes of certain widgets
        # the polish function will set some widgets to transparent mode, to get the
        # full benefit from the nice pixmaps in the color group.
        if w.inherits("QTipLabel"):
            return

        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)

    def unPolish_qwidget(self, w):
        # Undo what we did in polish_qwidget
        if w.inherits("QTipLabel"):
            return

        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
    #

    def drawPopupPanel(self, painter,x, y, w, h, colorGroup, lineWidth, fill):
        self.drawPanel(painter, x, y, w, h, colorGroup, FALSE, lineWidth, fill)

    def drawPanel(self, painter, x, y, w, h, colorGroup, sunken, lineWidth, fill):

        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)
        painter.setBrush(oldbrush)

    def drawRect(self, painter, x, y, w, h, color, lineWidth, fill):
        qDrawPlainRect(painter, x, y, w, h, color, lineWidth, fill)

    def drawRectStrong(self, painter, x, y, w, h, colorGroup,
                             sunken, lineWidth, midLineWidth, fill):
        qDrawPlainRect(painter, x, y, w, h, colorGroup.foreground(),
                       sunken, lineWidth *2, fill)

    def drawSeparator(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()

    def drawroundrect(self, painter, x, y, w, h):
        painter.drawRoundRect(x, y, w, h, 5, 50)

    def roundRectRegion(self, rect, r):
        x=rect.x()
        y=rect.y()
        right=x1+rect.right()
        bottom=y1+rect.bottom()
        a=QPointArray([8, x+r, y, right-r, y,
                      right, y + r, right, bottom-r,
                      right-r, bottom, x+r, bottom,
                      x, bottom-r, x, y+r])
        region=QRegion(a)

        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))
        region.unite(QRegion(right-d, bottom-d, r*2, r*2, QRegion.Ellipse))
        return region

    #
    # Tab
    #

    def drawTab(self, painter, tabBar, tab, selected):
        a=QPointArray(10)
        a.setPoint(0, 0, -1)
        a.setPoint(1, 0, 0)
        # Nasty! r is a private member of QTab. We shouldn't access it.
        y=tab.r.height()-2
        x=y/2
        x=x+1
        a.setPoint(2, x, y-1)
        x=x+1
        a.setPoint(3, x, y)
        x=x+1
        y=y+1
        a.setPoint(3, x, y)
        a.setPoint(4, x, y)

        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())

        painter.setPen(tabBar.colorGroup().foreground())
        painter.drawPolygon(a)
        painter.setBrush(Qt.NoBrush)

    def drawTabMask(self, painter, tabbar, tab, selected):
        painter.drawRect(tab.r)

    #
    # Sliders
    #

    def drawSlider(self, painter, x, y, w, h, colorGroup,
                         orientation, tickAbove, tickBelow):
        pass

    def drawSliderMask(self, painter, x, y, w, h,
                             orientation, tickAbove, tickBelow):
        painter.fillRect(x, y, w, h, Qt.color1)

    def drawSliderGrooveMask(self, painter, x, y, w, h, coord, orientation):

        colorGroup=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
    #

    def drawButton(self, painter, x, y, w, h, colorGroup,
                   sunken=FALSE, fill=None):
        oldBrush=painter.brush()

        if fill != None:
            painter.setBrush(fill)

        self.drawroundrect(painter, x, y, w, h)

        painter.setBrush(oldBrush)

    def drawPushButtonlabel (self, button, painter):
        QWindowsStyle.drawPushButonLabel(self, button, painter)

    def drawPushButton(self, button, painter):

        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

        if button.isOn() or button.isDown():
            sunken=TRUE
        else:
            sunken=FALSE

        self.drawButton(painter, x1, y1, x2-x1+1, y2-y1+1,
                        colorGroup, sunken, fill)

        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)

    def drawPushButtonLabel(self, button, painter):
        r=button.rect()
        (x, y, w, h)=r.rect()

        (x1, y1, x2, y2)=button.rect().coords()
        dx=0
        dy=0
        if button.isMenuButton():
            dx=(y2-y1)/3
        if dx or dy:
            p.translate(dx,dy)

        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,
                          Qt.AlignCenter|Qt.ShowPrefix,
                          g, button.isEnabled(),
                          button.pixmap(), button.text(), -1,
                          pencolour)

        if dx or dy:
            painter.translate(-dx,-dy)

    def drawBevelButton(self, painter, x, y, w, h, colorGroup,
                        sunken=FALSE, fill=None):
        self.drawButton(painter, x, y, w, h, colorGroup, sunken, fill)

    def buttonRect(self, x, y, w, h):
        return QRect(x+3, y+2, w-6, h-4)

    def drawButtonMask(self, p, x, y, w, h):
        self.drawroundrect(p, x, y, w, h)

    #
    # Radio Button
    #

    def drawExclusiveIndicator(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 drawExclusiveIndicatorMask(self, painter, x, y, w, h, on):
        painter.fillRect(x, y, w, h, QBrush(Qt.color1))

    #
    # Checkbox
    #

    def drawIndicator(self, painter, x, y, w, h, colorGroup,
                            state, down, enabled):
        painter.save()

        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()

    def drawIndicatorMask(self, painter, x, y, w, h, state):
        painter.fillRect(x, y , w + 3, h, QBrush(Qt.color1))

    #
    # Menu bar
    #

    def drawMenuBarItem(self, painter, x, y, w, h, menuItem, \
                        colorGroup, enabled, active):
        """
        Not subclassable?
        """
        self.drawItem(painter, x, y, w, h,
                      Qt.AlignCenter | Qt.ShowPrefix | Qt.DontClip | \
                      Qt.SingleLine, colorGroup, menuItem.pixmap(), \
                      menuItem.text(), -1, QColorGroup.buttonText())

    #
    # These items are not (yet) implemented in PyQt
    #

    def drawPopupMenuItem (self, painter, checkable, maxpmw, tab,
                                 menuItem, palette, act, enabled,
                                 x, y, w, h):
        """
        Not implemented in PyQt
        """
        pass

    def extraPopupMenuItemWidth (self, checkable, maxpmw,
                                       menuItem, fontMetrics):
        """
        Not implemented in PyQt
        """
        pass

    def popupMenuItemHeight (self, checkable, menuItem, fontMetrics):
        """
        Not implemented in PyQt
        """
        pass

Using styles from PyQt

Using these custom styles in your own application is as simple as using the built-in styles. I have adapted the small themes.py example that comes with BlackAdder or PyQt to show off the new custom styles:

Example 22-5. Testing styles

#
# styletester.py - a testbed for styles.
# Based on Phil's adaption of my translation of the
# Qt themes example app.
#

FALSE=0
TRUE=1

import os, sys
from qt import *
from macstyle import MacStyle

class ButtonsGroups(QVBox):

  def __init__(self, parent=None, name=None):
    QVBox.__init__(self, parent, name)

    # Create widgets which allow easy layouting
    box1=QHBox(self)
    box2=QHBox(self)

    # first group

    # Create an exclusive button group
    grp1=QButtonGroup( 1
                     , QGroupBox.Horizontal
                     , "Button Group 1 (exclusive)"
                     , box1
                     )
    grp1.setExclusive(TRUE)

    # insert 3 radiobuttons
    rb11=QRadioButton("&Radiobutton 1", grp1)
    rb11.setChecked(TRUE)
    QRadioButton("R&adiobutton 2", grp1)
    QRadioButton("Ra&diobutton 3", grp1)

    # second group

    # Create a non-exclusive buttongroup
    grp2=QButtonGroup( 1
                     , QGroupBox.Horizontal
                     , "Button Group 2 (non-exclusive)"
                     , box1
                     )
    grp2.setExclusive(FALSE)

    # 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

    # create a buttongroup which is exclusive for radiobuttons and
    # non-exclusive for all other buttons
    grp3=QButtonGroup( 1
                     , QGroupBox.Horizontal
                     , "Button Group 3 (Radiobutton-exclusive)"
                     , box2
                     )
    grp3.setRadioButtonExclusive(TRUE)

    # insert three radiobuttons
    self.rb21=QRadioButton("Rad&iobutton 1", grp3)
    self.rb22=QRadioButton("Radi&obutton 2", grp3)
    self.rb23=QRadioButton("Radio&button 3", grp3)
    self.rb23.setChecked(TRUE)

    # insert a checkbox...
    self.state=QCheckBox("E&nable Radiobuttons", grp3)
    self.state.setChecked(TRUE)
    # ...and connect its SIGNAL clicked() with the SLOT slotChangeGrp3State()
    self.connect(self.state, SIGNAL('clicked()'),self.slotChangeGrp3State)

    # fourth group

    # create a groupbox which lays out its childs in a column
    grp4=QGroupBox( 1
                    , QGroupBox.Horizontal
                    , "Groupbox with normal buttons"
                    , box2
                    )

    # insert two pushbuttons...
    QPushButton("&Push Button", grp4)
    bn=QPushButton("&Default Button", grp4)
    bn.setDefault(TRUE)
    tb=QPushButton("&Toggle Button", grp4)

    # ...and make the second one a toggle button
    tb.setToggleButton(TRUE)
    tb.setOn(TRUE)

  def slotChangeGrp3State(self):
    self.rb21.setEnabled(self.state.isChecked())
    self.rb22.setEnabled(self.state.isChecked())
    self.rb23.setEnabled(self.state.isChecked())

class LineEdits(QVBox):

  def __init__(self, parent=None, name=None):
    QVBox.__init__(self, parent, name)

    self.setMargin(10)

    # Widget for layouting
    row1=QHBox(self)
    row1.setMargin(5)

    # Create a label
    QLabel("Echo Mode: ", row1)

    # Create a Combobox with three items...
    self.combo1=QComboBox(FALSE, row1)
    self.combo1.insertItem("Normal", -1)
    self.combo1.insertItem("Password", -1)
    self.combo1.insertItem("No Echo", -1)
    self.connect(self.combo1, SIGNAL('activated(int)'), self.slotEchoChanged)

    # insert the first LineEdit
    self.lined1=QLineEdit(self)

    # another widget which is used for layouting
    row2=QHBox(self)
    row2.setMargin(5)

    # and the second label
    QLabel("Validator: ", row2)

    # A second Combobox with again three items...
    self.combo2=QComboBox(FALSE, row2)
    self.combo2.insertItem("No Validator", -1)
    self.combo2.insertItem("Integer Validator", -1)
    self.combo2.insertItem("Double Validator", -1)
    self.connect(self.combo2, SIGNAL('activated(int)'),
                 self.slotValidatorChanged)

    # and the second LineEdit
    self.lined2=QLineEdit(self)

    # yet another widget which is used for layouting
    row3=QHBox(self)
    row3.setMargin(5)

    # we need a label for this too
    QLabel("Alignment: ", row3)

    # A combo box for setting alignment
    self.combo3=QComboBox(FALSE, row3)
    self.combo3.insertItem("Left", -1)
    self.combo3.insertItem("Centered", -1)
    self.combo3.insertItem("Right", -1)
    self.connect(self.combo3, SIGNAL('activated(int)'),
                 self.slotAlignmentChanged)

    # and the lineedit
    self.lined3=QLineEdit(self)

    # give the first LineEdit the focus at the beginning
    self.lined1.setFocus()

  def slotEchoChanged(self, i):
    if i == 0:
      self.lined1.setEchoMode(QLineEdit.EchoMode.Normal)
    elif i == 1:
      self.lined1.setEchoMode(QLineEdit.EchoMode.Password)
    elif i == 2:
      self.lined1.setEchoMode(QLineEdit.EchoMode.NoEcho)

    self.lined1.setFocus()

  def slotValidatorChanged(self, i):
    if i == 0:
      self.validator=None
      self.lined2.setValidator(self.validator)
    elif i == 1:
      self.validator=QIntValidator(self.lined2)
      self.lined2.setValidator(self.validator)
    elif i == 2:
      self.validator=QDoubleValidator(-999.0, 999.0, 2, self.lined2)
      self.lined2.setValidator(self.validator)

    self.lined2.setText("")
    self.lined2.setFocus()

  def slotAlignmentChanged(self, i):
    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):

  def __init__(self, parent=None, name=None):
    QVBox.__init__(self, parent, name)

    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)

    # insert three radiobuttons which the user can use to set the speed
    # of the progress and two pushbuttons to start/pause/continue and
    # reset the  progress
    self.slow=QRadioButton("&Slow", bg)
    self.start=QPushButton("S&tart", bg)
    self.normal=QRadioButton("&Normal", bg)
    self.reset=QPushButton("&Reset", bg)
    self.fast=QRadioButton("&Fast", bg)

    # Create the progressbar
    self.progress=QProgressBar(100, self)

    # connect the clicked() SIGNALs of the pushbuttons to SLOTs
    self.connect(self.start, SIGNAL('clicked()'), self.slotStart)
    self.connect(self.reset, SIGNAL('clicked()'), self.slotReset)

    # connect the timeout() SIGNAL of the progress-timer to a SLOT
    self.connect(self.timer, SIGNAL('timeout()'), self.slotTimeout)

    # Let's start with normal speed...
    self.normal.setChecked(TRUE)

  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)

      # disable the speed-radiobuttons:
      self.slow.setEnabled(FALSE)
      self.normal.setEnabled(FALSE)
      self.fast.setEnabled(FALSE)

    # If the progress is not running...
    if not self.timer.isActive():
      # ...start the time (and so the progress) with an interval fo 1ms...
      self.timer.start(1)
      # ...and rename the start/pause/continue button to Pause
      self.start.setText("&Pause")
    else:
      # ...stop the timer (and so the progress)...
      self.timer.stop()
      # ...and rename the start/pause/continue button to Continue
      self.start.setText("&Continue")

  def slotReset(self):
    # stop the timer and progress
    self.timer.stop()

    # rename the start/pause/continue button to Start...
    self.start.setText("&Start")
    # ...and enable this button
    self.start.setEnabled(TRUE)

    # enable the speed-radiobuttons
    self.slow.setEnabled(TRUE)
    self.normal.setEnabled(TRUE)
    self.fast.setEnabled(TRUE)

    # reset the progressbar
    self.progress.reset()

  def slotTimeout(self):
    p = self.progress.progress()

    # If the progress is complete...
    if p == self.progress.totalSteps():
      # ...rename the start/pause/continue button to Start...
      self.start.setText("&Start")
      # ...and disable it...
      self.start.setEnabled(FALSE)
      # ...and return
      return

    # If the progress is not complete increase it
    self.progress.setProgress(p+1)

class ListBoxCombo(QVBox):

  def __init__(self, parent=None, name=None):
    QVBox.__init__(self, parent, name)

    self.setMargin(5)

    row1=QHBox(self)
    row1.setMargin(5)

    # Create a multi-selection ListBox...
    self.lb1=QListBox(row1)
    self.lb1.setMultiSelection(TRUE)

    # ...insert a pixmap item...
    self.lb1.insertItem(QPixmap("qtlogo.png"))
    # ...and 100 text items
    for i in range(100):
      str=QString("Listbox Item %1").arg(i)
      self.lb1.insertItem(str)

    # Create a pushbutton...
    self.arrow1=QPushButton(" -> ", row1)
    # ...and connect the clicked SIGNAL with the SLOT slotLeft2Right
    self.connect(self.arrow1, SIGNAL('clicked()'), self.slotLeft2Right)

    # create an empty single-selection ListBox
    self.lb2=QListBox(row1)

  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 second ListBox
          self.lb2.insertItem(QListBoxText(item.text()))
        # ...and if it is a pixmap item...
        elif item.pixmap():
          # ...insert an item with the same pixmap into the second ListBox
          self.lb2.insertItem(QListBoxPixmap(item.pixmap()))

class Themes(QMainWindow):

  def __init__(self, parent=None, name=None, f=Qt.WType_TopLevel):
    QMainWindow.__init__(self, parent, name, f)

    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)

    self.sMacStyle=self.style.insertItem("&Classic Mac", self.styleMac)
    self.sPlatinum=self.style.insertItem("&Platinum", self.stylePlatinum)
    self.sWindows=self.style.insertItem("&Windows", self.styleWindows)
    self.sCDE=self.style.insertItem("&CDE", self.styleCDE)
    self.sMotif=self.style.insertItem("M&otif", self.styleMotif)
    self.sMotifPlus=self.style.insertItem("Motif P&lus", \
    self.styleMotifPlus)
    self.style.insertSeparator()
    self.style.insertItem("&Quit", qApp.quit, Qt.CTRL | Qt.Key_Q)

    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)

  # In the following we cannot simply set the new style as we can in C++.
  # We need to keep the old style alive (if it is a Python one) so that it's
  # unPolish methods can still be called when the new style is set.

  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)
    qApp.setStyle(newstyle)
    self.style=newstyle
    self.selectStyleMenu(self.sCDE)

    p=QPalette(QColor(75, 123, 130))
    p.setColor(QPalette.Active, QColorGroup.Base, QColor(55, 77, 78));
    p.setColor(QPalette.Inactive, QColorGroup.Base, QColor(55, 77, 78));
    p.setColor(QPalette.Disabled, QColorGroup.Base, QColor(55, 77, 78));
    p.setColor(QPalette.Active, QColorGroup.Highlight, Qt.white);
    p.setColor(QPalette.Active, QColorGroup.HighlightedText, \
    QColor(55, 77, 78));
    p.setColor(QPalette.Inactive, QColorGroup.Highlight, Qt.white);
    p.setColor(QPalette.Inactive, QColorGroup.HighlightedText, \
    QColor(55, 77, 78));
    p.setColor(QPalette.Disabled, QColorGroup.Highlight, Qt.white);
    p.setColor(QPalette.Disabled, QColorGroup.HighlightedText, \
    QColor(55, 77, 78));
    p.setColor(QPalette.Active, QColorGroup.Foreground, Qt.white);
    p.setColor(QPalette.Active, QColorGroup.Text, Qt.white);
    p.setColor(QPalette.Active, QColorGroup.ButtonText, Qt.white);
    p.setColor(QPalette.Inactive, QColorGroup.Foreground, Qt.white);
    p.setColor(QPalette.Inactive, QColorGroup.Text, Qt.white);
    p.setColor(QPalette.Inactive, QColorGroup.ButtonText, Qt.white);
    p.setColor(QPalette.Disabled, QColorGroup.Foreground, Qt.lightGray);
    p.setColor(QPalette.Disabled, QColorGroup.Text, Qt.lightGray);
    p.setColor(QPalette.Disabled, QColorGroup.ButtonText, Qt.lightGray);
    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")

  def selectStyleMenu(self, s):
    self.menuBar().setItemChecked(self.sMacStyle, FALSE)
    self.menuBar().setItemChecked(self.sPlatinum, FALSE)
    self.menuBar().setItemChecked(self.sCDE, FALSE)
    self.menuBar().setItemChecked(self.sMotifPlus, FALSE)
    self.menuBar().setItemChecked(self.sMotif, FALSE)
    self.menuBar().setItemChecked(self.sWindows, FALSE)
    self.menuBar().setItemChecked(s, TRUE)

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, it's 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:

Some Qt widgets with an ancient Mac look.

Prev | Home | Next
Faking it with bitmaps | Up | Drag and drop


Initiating drags
Prev Chapter 23. Drag and drop Next

Initiating drags

Initiating drag operations is a bit more difficult, because you must determine when exactly a drag begins. Users expect that holding down the mouse-button on the object they want to drag and moving it should start dragging. However, everyone moves the mouse very slightly, even if just single-clicking on an object. Thus, we must determine how many pixels the mouse pointer should move (while the first mouse button is depressed) to enter a drag operation.

We cannot do this using QMultiLineEdit. A class like QIconView offers special support for starting drags by allowing you to re-implement the dragObject function:

class MyIconView(QIconView):

      def dragObject(self):
          return QTextDrag(self.currentItem().text(), self)

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 object—if 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:

Example 23-2. Drag and drop

#!/usr/bin/env python

from sys import argv
from qt import *

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 dragEnterEvent(self, event):
        event.accept(QTextDrag.canDecode(event))

    def dropEvent(self, event):
        text = QString()
        pos = QPoint(event.pos().x(), event.pos().y() - self.header().height())
        item = self.itemAt(pos)
        text=QString()

    def mouseMoveEvent(self, event):
        self.startDrag()

    def startDrag(self):
        d = QTextDrag("blabla",self) # keep a reference to d
        d.dragCopy()

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()

Prev | Home | Next
Drag and drop | Up | Conclusion


Conclusion
Prev Chapter 23. Drag and drop Next

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.

Prev | Home | Next
Initiating drags | Up | Printing


Adding printing to Kalam
Prev Chapter 24. Printing Next

Adding printing to Kalam

Adding a file menu entry and a toolbar button for the print functionality is easy enough. We don't need to have a separate printer setup and print dialog. We are going to show the printing setup dialog every time a user wants to print a document. Because we use one central QPrinter object, settings remain valid for the duration of the application. You might want to save the printer settings to the configuration file (I haven't done that here).

    # fragment from kalamapp.py

    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)

    ...

    def setActionsEnabled(self, *args):
        enabled = self.docManager.numberOfDocuments()
        ...
        self.actions["filePrint"].setEnabled(enabled)

    ...

    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."

Prev | Home | Next
Printing | Up | Putting ink to paper


Putting ink to paper
Prev Chapter 24. Printing Next

Putting ink to paper

The following is a very simplistic way of dumping text onto paper. We merely run through all lines in the active view and dump them on paper. If a line is too long for the chosen paper-size, we'll just let it run of the paper, we won't implement word-wrap.

    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

                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 user's 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, it's 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.

Prev | Home | Next
Adding printing to Kalam | Up | Conclusion


Conclusion
Prev Chapter 24. Printing Next

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 you're 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!

Prev | Home | Next
Putting ink to paper | Up | Internationalizing an Application


Packaging source
Prev Chapter 26. Delivering your Application Next

Packaging source

If you merely want to deliver the source to your PyQt application, you might as well create a nice package structure. You can zip the source-code up or tar it down, and deliver it. You'd 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.

Example 26-1. README

   Kalam - the undocumented, extensible editor.

Kalam is an editor written in Python and PyQt as an example
application to accompany the third part of the book "Gui programming
with Python and Qt", published by OpenDocs.

You can extend Kalam with macro's written in Python. See edmund.py
for an example. Consult either the book, or the file kalamconfig.py
to configure Kalam, using, for instance startup.py.

   Installation

In order to run Kalam, the kalam main directory needs to be on the
Python path. On Unix, you can add something like:
PYTHONPATH=/path/to/kalam/directory:$PYTHONPATH to .bashrc. On
Windows, you must edit that system variable using the Control Panel.

   Requirements:

you must have Python (>2.0) and PyQt (>2.4) installed. PyQt depends
on sip. You can get Python from www.python.org and PyQt from
www.thekompany.com.

Boudewijn Rempt
boud@rempt.xs4all.nl

This is the easiest way out for you as a developer. You can also use Python's distutils to create a source distribution of your application. Since this is the first step to a binary distribution, it's a good idea to use distutils, even for source-only distributions.

Prev | Home | Next
Delivering your Application | Up | Starting with distutils.


Starting with distutils.
Prev Chapter 26. Delivering your Application Next

Starting with distutils.

Once you've collected all the bits and bobs for your application, you start using distutils by writing a special Python script, customarily named setup.py. You can make your setup.py script as complex as you want, but typically it is quite simple.

The setup.py script will then call setup from distutils.core with lots of arguments, including meta-data about the application or module to install, and a list of stuff to install.

The developer uses the setup.py script to create the package, and the user uses it to install the package.

setup.py

Example 26-2. setup.py - a sample setup script

#!/usr/bin/env python

from distutils.core import setup

setup(name = "kalam",
      version = "1.0",
      description = "Kalam - the extensible Python editor",
      author = "Boudewijn Rempt",
      author_email = "boud@rempt.xs4all.nl",
      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. Let's examine all parts:

  • name: the name of the application

  • version: the version number (major, minor, wee number)

  • description: a short description of the application

  • author: the person responsible for the application

  • author_email: his email address

  • url: website where the application is hosted

  • packages: a list of Python modules (directories that contain a set of Python files listed in a __init__.py file). In the case of Kalam these are the modules for the character map, the type-o-meter and the set of workspace options. The additional module, kalamlib, contains all the real Kalam code.

  • py_modules: a list of Python files. Note that if you include both the packages and thepy_modules keyword only the latter will be used.

  • 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 won't 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 user's 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 don't 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.

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.

Example 26-3. MANIFEST.in

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. (The latter is not absolutely necessary for running the application, but it can't hurt to give people access to our dialog designs.)

The options available for MANIFEST.in are:

  • include pat1 pat2 ...

  • exclude pat1 pat2 ...

  • recursive-include dir pat1 pat2 ...

  • recursive-exclude dir pat1 pat2 ...

  • global-include pat1 pat2 ...

  • global-exclude pat1 pat2 ...

  • prune dir

  • graft dir

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

All Python files (everything that is mentioned in the py_modules or packages argument in setup.py) will be installed in the install_lib directory. Everything that is mentioned in the data_files argument will be installed in the install_data directory. Likewise, everything that is included in the scripts argument will be installed in install_scripts.

Creating the source distribution

We are now ready to create the source distribution. This is a simple, one-line command:

boud@calcifer:~/src/kalam > python setup.py sdist

The distutils will then spew a lot of text on the screen, and deliver your package in a subdirectory named dist:

boudewijn@maldar:~/doc/pyqt/ch18/kalam > python setup.py sdist
running sdist
reading manifest template 'MANIFEST.in'
writing manifest file 'MANIFEST'
creating kalam-1.0
creating kalam-1.0/charmap
creating kalam-1.0/data
creating kalam-1.0/dialogs
creating kalam-1.0/kalamlib
creating kalam-1.0/pixmaps
creating kalam-1.0/typometer
creating kalam-1.0/workspace
making hard links in kalam-1.0...
hard linking README -> kalam-1.0
hard linking edmund.py -> kalam-1.0
hard linking kalam -> kalam-1.0
hard linking kalam.bat -> kalam-1.0
hard linking setup.cfg -> kalam-1.0
hard linking setup.py -> kalam-1.0
hard linking sitecustomize.py -> kalam-1.0
hard linking startup.py -> kalam-1.0
hard linking charmap/__init__.py -> kalam-1.0/charmap
hard linking charmap/charmap.py -> kalam-1.0/charmap
hard linking data/Blocks.txt -> kalam-1.0/data
hard linking dialogs/frmfindreplace.ui -> kalam-1.0/dialogs
hard linking dialogs/frmsettings.ui -> kalam-1.0/dialogs
hard linking kalamlib/__init__.py -> kalam-1.0/kalamlib
hard linking kalamlib/configtest.py -> kalam-1.0/kalamlib
hard linking kalamlib/dlgfindreplace.py -> kalam-1.0/kalamlib
hard linking kalamlib/dlgsettings.py -> kalam-1.0/kalamlib
hard linking kalamlib/docmanager.py -> kalam-1.0/kalamlib
hard linking kalamlib/docmanagertest.py -> kalam-1.0/kalamlib
hard linking kalamlib/frmfindreplace.py -> kalam-1.0/kalamlib
hard linking kalamlib/frmsettings.py -> kalam-1.0/kalamlib
hard linking kalamlib/kalamapp.py -> kalam-1.0/kalamlib
hard linking kalamlib/kalamconfig.py -> kalam-1.0/kalamlib
hard linking kalamlib/kalamdoc.py -> kalam-1.0/kalamlib
hard linking kalamlib/kalamview.py -> kalam-1.0/kalamlib
hard linking kalamlib/macromanager.py -> kalam-1.0/kalamlib
hard linking kalamlib/macromanagertest.py -> kalam-1.0/kalamlib
hard linking kalamlib/main.py -> kalam-1.0/kalamlib
hard linking kalamlib/resources.py -> kalam-1.0/kalamlib
hard linking pixmaps/fileprint.xpm -> kalam-1.0/pixmaps
hard linking pixmaps/find.png -> kalam-1.0/pixmaps
hard linking pixmaps/listspace.png -> kalam-1.0/pixmaps
hard linking pixmaps/listspace.xpm -> kalam-1.0/pixmaps
hard linking pixmaps/splitspace.png -> kalam-1.0/pixmaps
hard linking pixmaps/splitspace.xpm -> kalam-1.0/pixmaps
hard linking pixmaps/stackspace.png -> kalam-1.0/pixmaps
hard linking pixmaps/stackspace.xpm -> kalam-1.0/pixmaps
hard linking pixmaps/tabmanager.png -> kalam-1.0/pixmaps
hard linking pixmaps/tabmanager.xpm -> kalam-1.0/pixmaps
hard linking pixmaps/workspace.png -> kalam-1.0/pixmaps
hard linking pixmaps/workspace.xpm -> kalam-1.0/pixmaps
hard linking typometer/__init__.py -> kalam-1.0/typometer
hard linking typometer/typometer.py -> kalam-1.0/typometer
hard linking workspace/__init__.py -> kalam-1.0/workspace
hard linking workspace/listspace.py -> kalam-1.0/workspace
hard linking workspace/splitspace.py -> kalam-1.0/workspace
hard linking workspace/stackspace.py -> kalam-1.0/workspace
hard linking workspace/tabmanager.py -> kalam-1.0/workspace
hard linking workspace/workspace.py -> kalam-1.0/workspace
creating dist
tar -cf dist/kalam-1.0.tar kalam-1.0
gzip -f9 dist/kalam-1.0.tar
removing 'kalam-1.0' (and everything under it)
boudewijn@maldar:~/doc/pyqt/ch18/kalam >

That's it—a nice, clean and complete source distribution of Kalam. You can generate both zip archives and gzipped tarballs by providing options on the command line:

boudewijn@maldar:~/doc/pyqt/ch18/kalam > python setup.py sdist  --formats=gztar,zip

The options are zip, gztar, bztar, ztar and tar, for zipfiles, gzipped tarfiles, bzipped tarfiles, compressed tarfiles and plain tar files.

Installing a source archive

Installing a source archive is a simple matter of unpacking the archive and executing the following command:

 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!.

Prev | Home | Next
Packaging source | Up | Creating Unix RPM packages


Creating Unix RPM packages
Prev Chapter 26. Delivering your Application Next

Creating Unix RPM packages

It is very easy to create installable RPM packages with distutils. RPM is one of the two or three standard package formats in the Unix world (the others are compressed or gzipped tar archives and Debian packages) and most Linux distributions support it.

RPM has strong support for dependency checking. This means you can create a package that cannot be installed unless certain other packages have been installed first. You can even demand that those packages are of a certain version number.

Creating an RPM takes just one command:

boudewijn@maldar:~/doc/pyqt/ch18/kalam > python setup.py bdist_rpm

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:

requires = python-2.2 pyqt-2.5

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.

Prev | Home | Next
Starting with distutils. | Up | Windows installers


Windows installers
Prev Chapter 26. Delivering your Application Next

Windows installers

As long as you haven't created any C or C++ extensions yourself, creating an executable Windows installer is as simple as calling:

boudewijn@maldar:~/doc/pyqt/ch18/kalam > python setup.py bdist_wininst

You can do this on a Unix system, too. The resulting executable, kalam-1.0.win32.exe, is a self-extracting, self-installing zip archive (this means that you need to have the zip utility installed in order to be able to build the package, since zip is not part of Python). Of course, before creating this archive, you must edit setup.cfg to make sure your application ends up in a folder that conforms to the Windows way of life.

Prev | Home | Next
Creating Unix RPM packages | Up | Desktop integration


Desktop integration
Prev Chapter 26. Delivering your Application Next

Desktop integration

Integrating your application in the users desktop environment demands some knowledge of that environment. For instance, if you have created an application for KDE, you should install the files to the correct place in the /opt/kde2 directory. You need to provide the .desktop files. The demands differ for Gnome, and they differ again for Windows and OS X, and they differ between versions of each OS, so I can't very well give guidelines for this part.

Prev | Home | Next
Windows installers | Up | Envoi


PyQwt
Prev Appendix B. PyQwt: Python Bindings for Qwt Next

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:

boud@calcifer:~/src > tar -xzf qwt-0.3.0.tar.gz
boud@calcifer:~/src > cd qwt-0.3.0
boud@calcifer:~/src/qwt-0.3.0 > ls
CHANGES  COPYING  CVS  doc  Doxyfile  Doxyfile.users  examples
      include  INSTALL  makefiles  propagate  qwt.pro  README
      README.QT  src
boud@calcifer:~/src/qwt-0.3.0 > cp makefiles/linux-g++ ./Makefile
boud@calcifer:~/src/qwt-0.3.0 > make
...
ln -s libqwt.so.0.3.0 libqwt.so
ln -s libqwt.so.0.3.0 libqwt.so.0
ln -s libqwt.so.0.3.0 libqwt.so.0.3
boud@calcifer:~/src/qwt-0.3.0 > nc README
boud@calcifer:~/src/qwt-0.3.0 > su
Password:
root@calcifer:/home/boud/src/qwt-0.3.0 > mv libqwt.* /usr/local/lib
root@calcifer:/home/boud/src/qwt-0.3.0 > mkdir /usr/local/include/qwt

This did the trick—at 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 scripts—here, I picked one to demonstrate to you the way it works.

#!/usr/bin/env python

#
# qwtdemo.py
#
# Demonstrates that you can plot NumPy arrays and lists of Python floats.
# NumPy arrays are more elegant and more than 20 times faster than lists.

import sys

from qt import *
from qwt import *
from Numeric import *

def drange(start, stop, step):
    start, stop, step = float(start), float(stop), float(step)
    size = int(round((stop-start)/step))
    result = [start]*size
    for i in xrange(size):
        result[i] += i*step
    return result

def lorentzian(x):
    return 1.0/(1.0+(x-5.0)**2)

class ListArrayDemo(QWidget):
    def __init__(self, *args):
        apply(QWidget.__init__, (self,) + args)

        # create a plot widget for NumPy arrays
        self.aplot = QwtPlot('Plot -- NumPy arrays', self)
        # calculate 2 NumPy arrays
        xa = arange(0.0, 10.0, 0.01)
        ya = lorentzian(xa)
        # insert a curve, make it red and copy the arrays
        ca = self.aplot.insertCurve('y = lorentzian(x)')
        self.aplot.setCurvePen(ca, QPen(Qt.red))
        self.aplot.setCurveData(ca, xa, ya)

        # create a plot widget for lists of Python floats
        self.lplot = QwtPlot('Plot -- List of Python floats', self)
        # calculate 2 lists of Python floats
        xl = drange(0.0, 10.0, 0.01)
        yl = map(lorentzian, xl)
        # insert a curve, make it blue and copy the lists
        cl = self.lplot.insertCurve('y = lorentzian(x)')
        self.lplot.setCurvePen(cl, QPen(Qt.blue))
        self.lplot.setCurveData(cl, xl, yl)

    def resizeEvent(self, e):
        x = e.size().width()
        y = e.size().height()/2
        self.aplot.resize(x, y)
    self.aplot.move(0, 0)
        self.lplot.resize(x, y)
        self.lplot.move(0, y)

# admire
app = QApplication(sys.argv)
demo = ListArrayDemo()
app.setMainWidget(demo)
demo.resize(400, 600)
demo.show()
app.exec_loop()

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.

Prev | Home | Next
PyQwt: Python Bindings for Qwt | Up | First Steps with Sip


How sip works
Prev Appendix C. First Steps with Sip Next

How sip works

Sip is in fact two things: a program, sip, that can be used to generate C++ wrapper code, and a small runtime library that handles much of the C++ functionality, such as signals and slots.

The program sip takes a set of input files that are created by the developer (.sip files) and uses them to generate a set of .h and .cpp files which are compiled and installed into a C++ library that Python knows how to communicate with.

In most cases, sip generates all C++ code automatically, but in some special cases it is necessary to manually write some wrapper code.

If you have the PyQt source distribution, you'll notice that you have a set of files whose names end with the .sip suffix, and another set of .h/.cpp files whose names begin with sip.

The .h/.cpp C++ files were generated from the .sip files using sip. These C++ files were then compiled to produce libraries that can be installed in the Python site-packages directory. Python can use these files to access the Qt library classes, methods and variables.

Prev | Home | Next
First Steps with Sip | Up | Creating .sip files


Creating .sip files
Prev Appendix C. First Steps with Sip Next

Creating .sip files

The easiest way to understand the creation of the .sip input files is to look at an example. The process begins with the original header file from the library source code distribution (in this case, qmultilinedit.h) which is used as the starting 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.

class QMultiLineEdit : QTableView
{
%HeaderCode
#include <qmultilinedit.h>
%End

public:
  QMultiLineEdit(QWidget * /TransferThis/ = 0,const char * = 0);

  int numLines() const;
  virtual void removeLine(int);

  void cursorPosition(int *,int *) const;
%MemberCode
  // The Python interface returns a tuple.

  QMultiLineEdit *ptr;

  if (sipParseArgs(&sipArgsParsed,sipArgs,
                   "m",
                   sipThisObj,sipClass_QMultiLineEdit,
                   &ptr))
  {
    int line, col;

    ptr -> QMultiLineEdit::cursorPosition(&line,&col);

    return Py_BuildValue("(ii)",line,col);
  }
%End

Most of the process of creating a .sip file is deleting all of the things SIP doesn't need or can't use. Typically all comments are stripped from the .h file in creating the .sip file, since they aren't 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:

Table C-1. C++ access specifiers and sip

C++ access | Use in .sip file
public | methods and variables
protected | methods only
private | methods only

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.

In the QMultiLineEdit constructor, the ‘parent' parameter name has been replaced with a /TransferThis/ directive. This directive tells sip that if the parameter is not None then ownership of the QMultiLineEdit object is transferred from PyQt to Qt. Therefore Qt is responsible for deleting this QMultiLineEdit object at the appropriate time. Failure to include this directive where needed would result in a segmentation fault, usually when the program terminates and Python tries to destroy the object which Qt has already destroyed. A /Transfer/ directive is also available to serve a similar purpose for an an object passed as a parameter to an ordinary function or method.

Directly following the class declaration is a %Headercode declaration that references the .h file that this .sip file was derived from. The %Headercode declaration goes inside the class definition because sip generates a .h/.cpp file for every class. If there were multiple classes defined in qmultilinedit.h, each class would require a %Headercode declaration. Sip itself doesn't use the .h file, but the sip generated code needs the .h file so it can know about the Qt classes and methods being wrapped.

Prev | Home | Next
How sip works | Up | Things sip can't do automatically


Things sip can't do automatically
Prev Appendix C. First Steps with Sip Next

Things sip can't do automatically

In a lot of cases, sip needs nothing but a stripped header file to do its work on—but sometimes more is needed.

Handwritten code

Look at the cursorPosition method. It does something Python can't do: it returns values via pointer arguments (the two int * parameters) passed to the method, instead of returning a tuple of values.

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:

>>> def f(a):
...     a.append("bla")
...
>>> a=["bla"]
>>> f(a)
>>> a
['bla', 'bla']

When you can a Python function with an immutable object like a string or an integer, the value of the reference outside the function won't change:

>>>
>>> def f(a):
...     a="b"
...
>>> a="a"
>>> f(a)
>>> a
'a'
>>>

That's 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;

    ptr -> QMultiLineEdit::cursorPosition(&line,&col);

    return Py_BuildValue("(ii)",line,col);
  }
%End

However, sip can't 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++ can't, and the member code is in the C++ domain.

If you look at some of the code sip generates, you'll 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 ‘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 don't need to run the sip program itself.

Other limitations

Presently, sip can't automatically generate code for methods which have parameters or return values with types like int&, int* or bool*.

In addition, sip can't directly handle types based on C++ templates. In these cases you have to used the %MappedType directive to supply your own C++ code to explicitly handle how such types are converted to and from Python objects. You still use the C++ template type in parameters and return values of functions and methods, sip will automatically carry out the necessary conversions based on the C++ code you provided.

Prev | Home | Next
Creating .sip files | Up | Where to look to start writing your own wrappers/bindings


Where to look to start writing your own wrappers/bindings
Prev Appendix C. First Steps with Sip Next

Where to look to start writing your own wrappers/bindings

It's quite possible to use sip to generate Python bindings for many types of C++ code, and not just Qt or KDE related code. SIP is most often used to wrap Qt-based third party libraries, like the QWT widgets.

However, Describing that process is beyond the scope of this book. This appendix is meant to help you get to grips with sip, but the fine details of actually building the wrapper library is very platform dependent.

If you want to explore further, download the CVS sources for PyQt (see the Section called Installing sip and PyQt without BlackAdder in Chapter 2, which include the make files for generating C++ files from .sip files (the PyQt source distribution has this done for you already and doesn't include these make files). The CVS sources also contain the qtmod.sip-in file, which is used to tell sip a number of things: the name of the module to build, the .sip files needed to build the module, version control, and the include files and linker information needed to 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.

Prev | Home | Next
Things sip can't do automatically | Up | Sip usage and syntax


Sip usage and syntax
Prev Appendix C. First Steps with Sip Next

Sip usage and syntax

Wilken Boie

This section has been prepared from the sip manual written by Wilken Boie, who has graciously given permission to use it in this book.

Usage

Invocation, Command Line

sip [-h] [-V] [-c dir] [-d file] [-m file] [-I dir] [-s suffix] [-p module] [file]

where:

Table C-2. Command line options

-h | display help message
-V | display the SIP version number
-c dir | the name of the code directory [default not generated]
-d file | the name of the documentation file [default not generated]
-m file | the name of the Makefile [default none generated]
-I dir | look in this directory when including files
-s suffix | the suffix to use for C++ source files [default ".cpp"]
-p module | the name of the generated C++ module [default Module]
file | the name of the specification file [default stdin]

Limitations

The following limitations are SIP compile time options:

  • Maximum number of arguments to a function: 20

  • Maximum nesting depth of %If directives: 10

  • Maximum number of nested version qualifiers in the generated Python: 10

  • Maximum number of option flags in function declarations: 5

Files

Source 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).

Files containing the wrapping

$(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):

# Python wrapper code
# Copying: Copyright (c) ....
import libMODc libMc.sipInitModule()

# here comes code from %PrePythonCode sections

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:

Intermediate Files

sip$(module)$(class).h, sip$(module)$(class).cpp

A pair of corresponding header and C++ files for each wrapped class.

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

Which contains the module global and initialisation code.

Auxilliary Files

makefile_name

If SIP option "-m makefile_name" is given, one of a set of makefile templates (defined in the SIP files) is seletected and instantiated with the appropriate module and class names. It is typically used to compile the wrapping.

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 can't 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 version that has already been run through the C++ pre-processor and has therefore lost it's 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 isn't going to cause complaints.

docfile_name

If SIP option "-m docfile_name" is given, documentation records are extracted from the SIP file(s) and written to docfile_name.

.sip File Syntax

General rules

  • All %keywords must start at beginning of line.

  • All _block_s must be closed by a matching %End directive.

  • Parameters are separated by whitespace, string parameters are enclosed by double quotes.

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:

Table C-3. Macros in Makefile Templates

$$ 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

Prev | Home | Next
Where to look to start writing your own wrappers/bindings | Up | Directives


Directives
Prev Appendix C. First Steps with Sip Next

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.

Documentation

Table of Contents
%Copying -- Start of software license _block_
%Doc -- Start of a documentation _block_
%ExportedDoc -- Start of an exported documentation _block_

Modules

Table of Contents
%Module -- Definition of the (main) modules name
%Include -- Include a file
%Import -- Import module (dynamic library)

Conditional Elements

Table of Contents
%If -- start of a conditional _block_
%End -- Terminate _block_
Version() -- Condition function for %If directive
%Version -- Define version value(s) and evaluation methods
%PrimaryVersions -- Define list of primary versions
%VersionCode -- Start of a version code _block_

C++ and Header Code Sections

Table of Contents
%HeaderCode -- Start of a C++ header code _block_
%ExportedHeaderCode -- Start of an exported C++ header code _block_

Mapped Classes

A mapped class

  • cannot have super classes, member functions or constructors.
  • cannot have a %ConvertToSubClassCode directive.
  • must have exactly one %ConvertFromClassCode directive.
  • must have exactly one %ConvertToClassCode directive.
  • must have exactly one %CanConvertToClassCode directive

Special Python methods

Table of Contents
PyMethods -- Implement additional Python special methods
PyNumberMethods -- Implement numerical Python special methods
PySequenceMethods -- Implement Python sequence special methods
PyMappingMethods -- Implement Python mapping special methods

Python special methods can be implemented, but code must be supplied. The following directives define additional sections in a class declaration (like public, private etc.). A typical sip file section is as follows:

    class X
    {
        ...
        PyMethods:
            __str__
    %MemberCode
                C++ code (no enclosing braces needed).
                Number and kind of arguments and return value depend on method
                (see specialPyMethod in sip.h).
    %End
        ...
    }

Other

Table of Contents
%Makefile -- The start of a Makefile code _block_
Prev | Home | Next
Sip usage and syntax | Up | Accepted C++ / Qt constructs


Accepted C++ / Qt constructs
Prev Appendix C. First Steps with Sip Next

Accepted C++ / Qt constructs

The following constructs can be handled by sip: typedef, struct, class, public, protected, private, signals, slots, virtual, const, static, void, bool, char, short, unsigned, int, long, float, double and enum. There are some restrictions:

class

  • Class definition can not be nested.
  • A class must not have private abstract functions.
  • Constructor must be in the public, private or protected sections.
  • Function must be in the public, private, protected, slot or signal sections.
  • Class variables must be in the public section.
  • Class enums must be in the public or protected sections.

Member functions

Static functions must be public and cannot be virtual.

The syntax of class method declarations is as follows:

name ([arg]..) [const] [=0] [/optflag[,optflag]../] ; [member_code]

Option flags can be given in a comma separated list of optflags enclosed in slashes. optflags can be boolean, or take a value. The following flags are recognized:

Table C-4. Flags in member function declaration

PyName=py_name | Use py_name instead of the C++ function name
ReleaseLock | Release the python interpreter lock
PreHook=hook_function | Define hook_function to be called prior to the memberfunc
PostHook=hook_function | Define hook_function to be called after the memberfunc
Transfer | Ownership is transferred
TransferThis | Ownership of this is transferred
TransferBack | Ownership is transferred back
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 don't 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

  • Static functions cannot be virtual.
  • Virtual signals aren't supported.
  • Arguments must be simple. Otherwise you have to supply your own C++ code.
const

  • Accepted for const function arguments and const functions.
static

  • %VariableCode cannot be specified for non-static class variables.
  • Cannot mix static and non-static member functions with the same Python name.
  • Static functions must be public and cannot be virtual.
Prev | Home | Next
Directives | Up | SIPLIB Functions


SIPLIB Functions
Prev Appendix C. First Steps with Sip Next

SIPLIB Functions

Public Support Functions

To use the SIPLIB functions described here, include sip.h. The source of these functions is in siplib.c.

Information functions

Table of Contents
sipGetCppPtr -- Get the C/C++ pointer from a wrapper and cast it to the required type
sipGetComplexCppPtr -- Get the C/C++ pointer for a complex object
sipGetThisWrapper -- Convert a C/C++ pointer to the object that wraps it
sipIsSubClassInstance -- See if a Python object is an instance of a sub-class of a given base class

Conversions and argument parsing

Table of Contents
sipParseArgs -- Parse the arguments to a C/C++ function without any side effects
sipConvertToCpp -- Convert a Python instance of a class to a C/C++ object pointer
sipMapCppToSelf -- Convert a C/C++ pointer to a Python instance
sipConvertToVoidPtr -- A convenience function to convert a C/C++ void pointer from a Python object
sipConvertFromVoidPtr -- A convenience function to convert a C/C++ void pointer to a Python object
sipConvertFromBool -- A convenience function to convert a C/C++ boolean to a Python object
sipCheckNone -- Check a None argument for a class pointer that we might dereference
sipBadVirtualResultType -- Report a Python member function with an unexpected return type
sipBadSetType -- Report a Python class variable with an unexpected type

Ressource handling

Table of Contents
sipReleaseLock -- Release the interpreter lock and save the current Python thread state
sipAcquireLock -- Acquire the interpreter lock and restore the Python thread state
sipCondReleaseLock -- Release the interpreter lock, if previously acquired, and save Python thread state
sipCondAcquireLock -- Acquire the interpreter lock, if not already acquired, and restore Python thread state
sipMalloc -- A Python 1.5 style memory allocator that supports Python 1.5 and 1.6
sipFree -- A Python 1.5 style memory de-allocator that supports Python 1.5 and 1.6

Calling Python

Table of Contents
sipEvalMethod -- Call a Python method
sipCallHook -- Call a hook

Functions specifically for signals/slots

Table of Contents
sipEmitSignal -- Emit a Python or Qt signal
sipConvertRx -- Convert a Python receiver to a Qt receiver
sipConnectRx -- Connect a Qt or a Python signal
sipGetRx -- Convert a valid Python signal or slot to an existing proxy Qt slot
sipDisconnectRx -- Disconnect a Qt or Python signal from a Python slot

These functions are specific for the Qt support. They are in the separate files sipqt.h and qtlib.cpp.

Private Functions

The private functions, which should only be called by generated code, are not documented here. For further information see the headerfile sip.h).

Prev | Home | Next
Accepted C++ / Qt constructs | Up | Bibliography


Introduction to the BlackAdder IDE

Prev Next

I. Introduction to the BlackAdder IDE

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 I'll 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.

Table of Contents
2. Installation
3. Interface
4. Introduction to Python
5. Debugging
Prev | Home | Next
About the BlackAdder IDE |  | Installation


PyQt fundamentals

Prev Next

II. PyQt fundamentals

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.

Table of Contents
6. Qt Concepts
7. Signals and Slots in Depth
8. String Objects in Python and Qt
9. Python Objects and Qt Objects
10. Qt Class Hierarchy
11. Qt Designer, BlackAdder and uic
Prev | Home | Next
If all else fails |  | Qt Concepts


Creating real applications with PyQt

Prev Next

III. Creating real applications with PyQt

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 won't use every single widget that PyQt offers us.

Table of Contents
12. Application Frameworks
13. Actions: menus, toolbars and accelerators
14. Automatic testing with PyUnit
15. A More Complex Framework: Multiple Documents, Multiple Views
16. User Interface Paradigms
17. Creating Application Functionality
18. Application Configuration
19. Using Dialog Windows
20. A Macro Language for Kalam
21. Drawing on Painters and Canvases
22. Gui Design in the Baroque Age
23. Drag and drop
24. Printing
25. Internationalizing an Application
26. Delivering your Application
27. Envoi
Prev | Home | Next
Advanced Designer topics |  | Application Frameworks


Appendices

Prev Next

IV. Appendices

Table of Contents
A. Reading the Qt Documentation
B. PyQwt: Python Bindings for Qwt
C. First Steps with Sip
Prev | Home | Next
Envoi |  | Reading the Qt Documentation


SIPLIB Functions

Public Support Functions

To use the SIPLIB functions described here, include sip.h. The source of these functions is in siplib.c.

Information functions

Table of Contents
sipGetCppPtr -- Get the C/C++ pointer from a wrapper and cast it to the required type
sipGetComplexCppPtr -- Get the C/C++ pointer for a complex object
sipGetThisWrapper -- Convert a C/C++ pointer to the object that wraps it
sipIsSubClassInstance -- See if a Python object is an instance of a sub-class of a given base class

Conversions and argument parsing

Table of Contents
sipParseArgs -- Parse the arguments to a C/C++ function without any side effects
sipConvertToCpp -- Convert a Python instance of a class to a C/C++ object pointer
sipMapCppToSelf -- Convert a C/C++ pointer to a Python instance
sipConvertToVoidPtr -- A convenience function to convert a C/C++ void pointer from a Python object
sipConvertFromVoidPtr -- A convenience function to convert a C/C++ void pointer to a Python object
sipConvertFromBool -- A convenience function to convert a C/C++ boolean to a Python object
sipCheckNone -- Check a None argument for a class pointer that we might dereference
sipBadVirtualResultType -- Report a Python member function with an unexpected return type
sipBadSetType -- Report a Python class variable with an unexpected type

Ressource handling

Table of Contents
sipReleaseLock -- Release the interpreter lock and save the current Python thread state
sipAcquireLock -- Acquire the interpreter lock and restore the Python thread state
sipCondReleaseLock -- Release the interpreter lock, if previously acquired, and save Python thread state
sipCondAcquireLock -- Acquire the interpreter lock, if not already acquired, and restore Python thread state
sipMalloc -- A Python 1.5 style memory allocator that supports Python 1.5 and 1.6
sipFree -- A Python 1.5 style memory de-allocator that supports Python 1.5 and 1.6

Calling Python

Table of Contents
sipEvalMethod -- Call a Python method
sipCallHook -- Call a hook

Functions specifically for signals/slots

Table of Contents
sipEmitSignal -- Emit a Python or Qt signal
sipConvertRx -- Convert a Python receiver to a Qt receiver
sipConnectRx -- Connect a Qt or a Python signal
sipGetRx -- Convert a valid Python signal or slot to an existing proxy Qt slot
sipDisconnectRx -- Disconnect a Qt or Python signal from a Python slot

These functions are specific for the Qt support. They are in the separate files sipqt.h and qtlib.cpp.

Private Functions

The private functions, which should only be called by generated code, are not documented here. For further information see the headerfile sip.h).

Prev | Home | Next
Accepted C++ / Qt constructs | Up | Bibliography