4330
|
1 |
#Copyright ReportLab Europe Ltd. 2000-2017
|
3062
|
2 |
#see license.txt for license details
|
4252
|
3 |
__version__='3.3.0'
|
3062
|
4 |
from tools.docco.rl_doc_utils import *
|
|
5 |
from reportlab.graphics.shapes import *
|
|
6 |
from reportlab.graphics.widgets import signsandsymbols
|
|
7 |
|
|
8 |
heading2("Widgets")
|
|
9 |
|
|
10 |
disc("""
|
|
11 |
We now describe widgets and how they relate to shapes.
|
|
12 |
Using many examples it is shown how widgets make reusable
|
|
13 |
graphics components.
|
|
14 |
""")
|
|
15 |
|
|
16 |
|
|
17 |
heading3("Shapes vs. Widgets")
|
|
18 |
|
|
19 |
disc("""Up until now, Drawings have been 'pure data'. There is no code in them
|
|
20 |
to actually do anything, except assist the programmer in checking and
|
|
21 |
inspecting the drawing. In fact, that's the cornerstone of the whole
|
|
22 |
concept and is what lets us achieve portability - a renderer only
|
|
23 |
needs to implement the primitive shapes.""")
|
|
24 |
|
|
25 |
disc("""We want to build reusable graphic objects, including a powerful chart
|
|
26 |
library. To do this we need to reuse more tangible things than
|
|
27 |
rectangles and circles. We should be able to write objects for other
|
|
28 |
to reuse - arrows, gears, text boxes, UML diagram nodes, even fully
|
|
29 |
fledged charts.""")
|
|
30 |
|
|
31 |
disc("""
|
|
32 |
The Widget standard is a standard built on top of the shapes module.
|
|
33 |
Anyone can write new widgets, and we can build up libraries of them.
|
|
34 |
Widgets support the $getProperties()$ and $setProperties()$ methods,
|
|
35 |
so you can inspect and modify as well as document them in a uniform
|
|
36 |
way.
|
|
37 |
""")
|
|
38 |
|
|
39 |
bullet("A widget is a reusable shape ")
|
|
40 |
bullet("""it can be initialized with no arguments
|
|
41 |
when its $draw()$ method is called it creates a primitive Shape or a
|
|
42 |
Group to represent itself""")
|
|
43 |
bullet("""It can have any parameters you want, and they can drive the way it is
|
|
44 |
drawn""")
|
|
45 |
bullet("""it has a $demo()$ method which should return an attractively drawn
|
|
46 |
example of itself in a 200x100 rectangle. This is the cornerstone of
|
|
47 |
the automatic documentation tools. The $demo()$ method should also have
|
|
48 |
a well written docstring, since that is printed too!""")
|
|
49 |
|
|
50 |
disc("""Widgets run contrary to the idea that a drawing is just a bundle of
|
|
51 |
shapes; surely they have their own code? The way they work is that a
|
|
52 |
widget can convert itself to a group of primitive shapes. If some of
|
|
53 |
its components are themselves widgets, they will get converted too.
|
|
54 |
This happens automatically during rendering; the renderer will not see
|
|
55 |
your chart widget, but just a collection of rectangles, lines and
|
|
56 |
strings. You can also explicitly 'flatten out' a drawing, causing all
|
|
57 |
widgets to be converted to primitives.""")
|
|
58 |
|
|
59 |
|
|
60 |
heading3("Using a Widget")
|
|
61 |
|
|
62 |
disc("""
|
|
63 |
Let's imagine a simple new widget.
|
|
64 |
We will use a widget to draw a face, then show how it was implemented.""")
|
|
65 |
|
|
66 |
eg("""
|
|
67 |
>>> from reportlab.lib import colors
|
|
68 |
>>> from reportlab.graphics import shapes
|
|
69 |
>>> from reportlab.graphics import widgetbase
|
|
70 |
>>> from reportlab.graphics import renderPDF
|
|
71 |
>>> d = shapes.Drawing(200, 100)
|
|
72 |
>>> f = widgetbase.Face()
|
|
73 |
>>> f.skinColor = colors.yellow
|
|
74 |
>>> f.mood = "sad"
|
|
75 |
>>> d.add(f)
|
|
76 |
>>> renderPDF.drawToFile(d, 'face.pdf', 'A Face')
|
|
77 |
""")
|
|
78 |
|
|
79 |
from reportlab.graphics import widgetbase
|
|
80 |
d = Drawing(200, 120)
|
|
81 |
f = widgetbase.Face()
|
|
82 |
f.x = 50
|
|
83 |
f.y = 10
|
|
84 |
f.skinColor = colors.yellow
|
|
85 |
f.mood = "sad"
|
|
86 |
d.add(f)
|
|
87 |
draw(d, 'A sample widget')
|
|
88 |
|
|
89 |
disc("""
|
|
90 |
Let's see what properties it has available, using the $setProperties()$
|
|
91 |
method we have seen earlier:
|
|
92 |
""")
|
|
93 |
|
|
94 |
eg("""
|
|
95 |
>>> f.dumpProperties()
|
|
96 |
eyeColor = Color(0.00,0.00,1.00)
|
|
97 |
mood = sad
|
|
98 |
size = 80
|
|
99 |
skinColor = Color(1.00,1.00,0.00)
|
|
100 |
x = 10
|
|
101 |
y = 10
|
|
102 |
>>>
|
|
103 |
""")
|
|
104 |
|
|
105 |
disc("""
|
|
106 |
One thing which seems strange about the above code is that we did not
|
|
107 |
set the size or position when we made the face.
|
|
108 |
This is a necessary trade-off to allow a uniform interface for
|
|
109 |
constructing widgets and documenting them - they cannot require
|
|
110 |
arguments in their $__init__()$ method.
|
|
111 |
Instead, they are generally designed to fit in a 200 x 100
|
|
112 |
window, and you move or resize them by setting properties such as
|
|
113 |
x, y, width and so on after creation.
|
|
114 |
""")
|
|
115 |
|
|
116 |
disc("""
|
|
117 |
In addition, a widget always provides a $demo()$ method.
|
|
118 |
Simple ones like this always do something sensible before setting
|
|
119 |
properties, but more complex ones like a chart would not have any
|
|
120 |
data to plot.
|
|
121 |
The documentation tool calls $demo()$ so that your fancy new chart
|
|
122 |
class can create a drawing showing what it can do.
|
|
123 |
""")
|
|
124 |
|
|
125 |
disc("""
|
|
126 |
Here are a handful of simple widgets available in the module
|
|
127 |
<i>signsandsymbols.py</i>:
|
|
128 |
""")
|
|
129 |
|
|
130 |
from reportlab.graphics.shapes import Drawing
|
|
131 |
from reportlab.graphics.widgets import signsandsymbols
|
|
132 |
|
|
133 |
d = Drawing(230, 230)
|
|
134 |
|
|
135 |
ne = signsandsymbols.NoEntry()
|
|
136 |
ds = signsandsymbols.DangerSign()
|
|
137 |
fd = signsandsymbols.FloppyDisk()
|
|
138 |
ns = signsandsymbols.NoSmoking()
|
|
139 |
|
|
140 |
ne.x, ne.y = 10, 10
|
|
141 |
ds.x, ds.y = 120, 10
|
|
142 |
fd.x, fd.y = 10, 120
|
|
143 |
ns.x, ns.y = 120, 120
|
|
144 |
|
|
145 |
d.add(ne)
|
|
146 |
d.add(ds)
|
|
147 |
d.add(fd)
|
|
148 |
d.add(ns)
|
|
149 |
|
|
150 |
draw(d, 'A few samples from signsandsymbols.py')
|
|
151 |
|
|
152 |
disc("""
|
|
153 |
And this is the code needed to generate them as seen in the drawing above:
|
|
154 |
""")
|
|
155 |
|
|
156 |
eg("""
|
|
157 |
from reportlab.graphics.shapes import Drawing
|
|
158 |
from reportlab.graphics.widgets import signsandsymbols
|
|
159 |
|
|
160 |
d = Drawing(230, 230)
|
|
161 |
|
|
162 |
ne = signsandsymbols.NoEntry()
|
|
163 |
ds = signsandsymbols.DangerSign()
|
|
164 |
fd = signsandsymbols.FloppyDisk()
|
|
165 |
ns = signsandsymbols.NoSmoking()
|
|
166 |
|
|
167 |
ne.x, ne.y = 10, 10
|
|
168 |
ds.x, ds.y = 120, 10
|
|
169 |
fd.x, fd.y = 10, 120
|
|
170 |
ns.x, ns.y = 120, 120
|
|
171 |
|
|
172 |
d.add(ne)
|
|
173 |
d.add(ds)
|
|
174 |
d.add(fd)
|
|
175 |
d.add(ns)
|
|
176 |
""")
|
|
177 |
|
|
178 |
|
|
179 |
heading3("Compound Widgets")
|
|
180 |
|
|
181 |
disc("""Let's imagine a compound widget which draws two faces side by side.
|
|
182 |
This is easy to build when you have the Face widget.""")
|
|
183 |
|
|
184 |
eg("""
|
|
185 |
>>> tf = widgetbase.TwoFaces()
|
|
186 |
>>> tf.faceOne.mood
|
|
187 |
'happy'
|
|
188 |
>>> tf.faceTwo.mood
|
|
189 |
'sad'
|
|
190 |
>>> tf.dumpProperties()
|
|
191 |
faceOne.eyeColor = Color(0.00,0.00,1.00)
|
|
192 |
faceOne.mood = happy
|
|
193 |
faceOne.size = 80
|
|
194 |
faceOne.skinColor = None
|
|
195 |
faceOne.x = 10
|
|
196 |
faceOne.y = 10
|
|
197 |
faceTwo.eyeColor = Color(0.00,0.00,1.00)
|
|
198 |
faceTwo.mood = sad
|
|
199 |
faceTwo.size = 80
|
|
200 |
faceTwo.skinColor = None
|
|
201 |
faceTwo.x = 100
|
|
202 |
faceTwo.y = 10
|
|
203 |
>>>
|
|
204 |
""")
|
|
205 |
|
|
206 |
disc("""The attributes 'faceOne' and 'faceTwo' are deliberately exposed so you
|
|
207 |
can get at them directly. There could also be top-level attributes,
|
|
208 |
but there aren't in this case.""")
|
|
209 |
|
|
210 |
|
|
211 |
heading3("Verifying Widgets")
|
|
212 |
|
|
213 |
disc("""The widget designer decides the policy on verification, but by default
|
|
214 |
they work like shapes - checking every assignment - if the designer
|
|
215 |
has provided the checking information.""")
|
|
216 |
|
|
217 |
|
|
218 |
heading3("Implementing Widgets")
|
|
219 |
|
|
220 |
disc("""We tried to make it as easy to implement widgets as possible. Here's
|
|
221 |
the code for a Face widget which does not do any type checking:""")
|
|
222 |
|
|
223 |
eg("""
|
|
224 |
class Face(Widget):
|
|
225 |
\"\"\"This draws a face with two eyes, mouth and nose.\"\"\"
|
|
226 |
|
|
227 |
def __init__(self):
|
|
228 |
self.x = 10
|
|
229 |
self.y = 10
|
|
230 |
self.size = 80
|
|
231 |
self.skinColor = None
|
|
232 |
self.eyeColor = colors.blue
|
|
233 |
self.mood = 'happy'
|
|
234 |
|
|
235 |
def draw(self):
|
|
236 |
s = self.size # abbreviate as we will use this a lot
|
|
237 |
g = shapes.Group()
|
|
238 |
g.transform = [1,0,0,1,self.x, self.y]
|
|
239 |
# background
|
|
240 |
g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5,
|
|
241 |
fillColor=self.skinColor))
|
|
242 |
# CODE OMITTED TO MAKE MORE SHAPES
|
|
243 |
return g
|
|
244 |
""")
|
|
245 |
|
|
246 |
disc("""We left out all the code to draw the shapes in this document, but you
|
|
247 |
can find it in the distribution in $widgetbase.py$.""")
|
|
248 |
|
|
249 |
disc("""By default, any attribute without a leading underscore is returned by
|
|
250 |
setProperties. This is a deliberate policy to encourage consistent
|
|
251 |
coding conventions.""")
|
|
252 |
|
|
253 |
disc("""Once your widget works, you probably want to add support for
|
|
254 |
verification. This involves adding a dictionary to the class called
|
|
255 |
$_verifyMap$, which map from attribute names to 'checking functions'.
|
|
256 |
The $widgetbase.py$ module defines a bunch of checking functions with names
|
|
257 |
like $isNumber$, $isListOfShapes$ and so on. You can also simply use $None$,
|
|
258 |
which means that the attribute must be present but can have any type.
|
|
259 |
And you can and should write your own checking functions. We want to
|
|
260 |
restrict the "mood" custom attribute to the values "happy", "sad" or
|
|
261 |
"ok". So we do this:""")
|
|
262 |
|
|
263 |
eg("""
|
|
264 |
class Face(Widget):
|
|
265 |
\"\"\"This draws a face with two eyes. It exposes a
|
|
266 |
couple of properties to configure itself and hides
|
|
267 |
all other details\"\"\"
|
|
268 |
def checkMood(moodName):
|
|
269 |
return (moodName in ('happy','sad','ok'))
|
|
270 |
_verifyMap = {
|
|
271 |
'x': shapes.isNumber,
|
|
272 |
'y': shapes.isNumber,
|
|
273 |
'size': shapes.isNumber,
|
|
274 |
'skinColor':shapes.isColorOrNone,
|
|
275 |
'eyeColor': shapes.isColorOrNone,
|
|
276 |
'mood': checkMood
|
|
277 |
}
|
|
278 |
""")
|
|
279 |
|
|
280 |
disc("""This checking will be performed on every attribute assignment; or, if
|
|
281 |
$config.shapeChecking$ is off, whenever you call $myFace.verify()$.""")
|
|
282 |
|
|
283 |
|
|
284 |
heading3("Documenting Widgets")
|
|
285 |
|
|
286 |
disc("""
|
|
287 |
We are working on a generic tool to document any Python package or
|
|
288 |
module; this is already checked into ReportLab and will be used to
|
|
289 |
generate a reference for the ReportLab package.
|
|
290 |
When it encounters widgets, it adds extra sections to the
|
|
291 |
manual including:""")
|
|
292 |
|
|
293 |
bullet("the doc string for your widget class ")
|
|
294 |
bullet("the code snippet from your <i>demo()</i> method, so people can see how to use it")
|
|
295 |
bullet("the drawing produced by the <i>demo()</i> method ")
|
|
296 |
bullet("the property dump for the widget in the drawing. ")
|
|
297 |
|
|
298 |
disc("""
|
|
299 |
This tool will mean that we can have guaranteed up-to-date
|
|
300 |
documentation on our widgets and charts, both on the web site
|
|
301 |
and in print; and that you can do the same for your own widgets,
|
|
302 |
too!
|
|
303 |
""")
|
|
304 |
|
|
305 |
|
|
306 |
heading3("Widget Design Strategies")
|
|
307 |
|
|
308 |
disc("""We could not come up with a consistent architecture for designing
|
|
309 |
widgets, so we are leaving that problem to the authors! If you do not
|
|
310 |
like the default verification strategy, or the way
|
|
311 |
$setProperties/getProperties$ works, you can override them yourself.""")
|
|
312 |
|
|
313 |
disc("""For simple widgets it is recommended that you do what we did above:
|
|
314 |
select non-overlapping properties, initialize every property on
|
|
315 |
$__init__$ and construct everything when $draw()$ is called. You can
|
|
316 |
instead have $__setattr__$ hooks and have things updated when certain
|
|
317 |
attributes are set. Consider a pie chart. If you want to expose the
|
|
318 |
individual wedges, you might write code like this:""")
|
|
319 |
|
|
320 |
eg("""
|
|
321 |
from reportlab.graphics.charts import piecharts
|
|
322 |
pc = piecharts.Pie()
|
|
323 |
pc.defaultColors = [navy, blue, skyblue] #used in rotation
|
|
324 |
pc.data = [10,30,50,25]
|
|
325 |
pc.slices[7].strokeWidth = 5
|
|
326 |
""")
|
|
327 |
#removed 'pc.backColor = yellow' from above code example
|
|
328 |
|
|
329 |
disc("""The last line is problematic as we have only created four wedges - in
|
|
330 |
fact we might not have created them yet. Does $pc.wedges[7]$ raise an
|
|
331 |
error? Is it a prescription for what should happen if a seventh wedge
|
|
332 |
is defined, used to override the default settings? We dump this
|
|
333 |
problem squarely on the widget author for now, and recommend that you
|
|
334 |
get a simple one working before exposing 'child objects' whose
|
|
335 |
existence depends on other properties' values :-)""")
|
|
336 |
|
|
337 |
disc("""We also discussed rules by which parent widgets could pass properties
|
|
338 |
to their children. There seems to be a general desire for a global way
|
|
339 |
to say that 'all wedges get their lineWidth from the lineWidth of
|
|
340 |
their parent' without a lot of repetitive coding. We do not have a
|
|
341 |
universal solution, so again leave that to widget authors. We hope
|
|
342 |
people will experiment with push-down, pull-down and pattern-matching
|
|
343 |
approaches and come up with something nice. In the meantime, we
|
|
344 |
certainly can write monolithic chart widgets which work like the ones
|
|
345 |
in, say, Visual Basic and Delphi.""")
|
|
346 |
|
|
347 |
disc("""For now have a look at the following sample code using an early
|
|
348 |
version of a pie chart widget and the output it generates:""")
|
|
349 |
|
|
350 |
eg("""
|
|
351 |
from reportlab.lib.colors import *
|
|
352 |
from reportlab.graphics import shapes,renderPDF
|
|
353 |
from reportlab.graphics.charts.piecharts import Pie
|
|
354 |
|
|
355 |
d = Drawing(400,200)
|
|
356 |
d.add(String(100,175,"Without labels", textAnchor="middle"))
|
|
357 |
d.add(String(300,175,"With labels", textAnchor="middle"))
|
|
358 |
|
|
359 |
pc = Pie()
|
|
360 |
pc.x = 25
|
|
361 |
pc.y = 50
|
|
362 |
pc.data = [10,20,30,40,50,60]
|
|
363 |
pc.slices[0].popout = 5
|
|
364 |
d.add(pc, 'pie1')
|
|
365 |
|
|
366 |
pc2 = Pie()
|
|
367 |
pc2.x = 150
|
|
368 |
pc2.y = 50
|
|
369 |
pc2.data = [10,20,30,40,50,60]
|
|
370 |
pc2.labels = ['a','b','c','d','e','f']
|
|
371 |
d.add(pc2, 'pie2')
|
|
372 |
|
|
373 |
pc3 = Pie()
|
|
374 |
pc3.x = 275
|
|
375 |
pc3.y = 50
|
|
376 |
pc3.data = [10,20,30,40,50,60]
|
|
377 |
pc3.labels = ['a','b','c','d','e','f']
|
|
378 |
pc3.wedges.labelRadius = 0.65
|
|
379 |
pc3.wedges.fontName = "Helvetica-Bold"
|
|
380 |
pc3.wedges.fontSize = 16
|
|
381 |
pc3.wedges.fontColor = colors.yellow
|
|
382 |
d.add(pc3, 'pie3')
|
|
383 |
""")
|
|
384 |
|
|
385 |
# Hack to force a new paragraph before the todo() :-(
|
|
386 |
disc("")
|
|
387 |
|
|
388 |
from reportlab.lib.colors import *
|
|
389 |
from reportlab.graphics import shapes,renderPDF
|
|
390 |
from reportlab.graphics.charts.piecharts import Pie
|
|
391 |
|
|
392 |
d = Drawing(400,200)
|
|
393 |
d.add(String(100,175,"Without labels", textAnchor="middle"))
|
|
394 |
d.add(String(300,175,"With labels", textAnchor="middle"))
|
|
395 |
|
|
396 |
pc = Pie()
|
|
397 |
pc.x = 25
|
|
398 |
pc.y = 50
|
|
399 |
pc.data = [10,20,30,40,50,60]
|
|
400 |
pc.slices[0].popout = 5
|
|
401 |
d.add(pc, 'pie1')
|
|
402 |
|
|
403 |
pc2 = Pie()
|
|
404 |
pc2.x = 150
|
|
405 |
pc2.y = 50
|
|
406 |
pc2.data = [10,20,30,40,50,60]
|
|
407 |
pc2.labels = ['a','b','c','d','e','f']
|
|
408 |
d.add(pc2, 'pie2')
|
|
409 |
|
|
410 |
pc3 = Pie()
|
|
411 |
pc3.x = 275
|
|
412 |
pc3.y = 50
|
|
413 |
pc3.data = [10,20,30,40,50,60]
|
|
414 |
pc3.labels = ['a','b','c','d','e','f']
|
|
415 |
pc3.slices.labelRadius = 0.65
|
|
416 |
pc3.slices.fontName = "Helvetica-Bold"
|
|
417 |
pc3.slices.fontSize = 16
|
|
418 |
pc3.slices.fontColor = colors.yellow
|
|
419 |
d.add(pc3, 'pie3')
|
|
420 |
|
|
421 |
draw(d, 'Some sample Pies')
|