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.
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 evQMouseEvent. 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
CharsetBrowserQCanvasView widget. Every time a new
script is selected, a new
CharsetCanvas is created — this
is easier than erasing the contents of the existing
canvas.
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.
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.
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.textQString 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
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).