draw 0.0.71
----------------------
The draw module is intended to provide simple dynamic user-interfaces
on low-level GUI-elements using an object-orientated interface.
The basic aproach is that you create a wrapper for the platform you are
working on that can be used by the classes in the module.
Content of this document
-----------------------------------
See also the
inheritance-tree.
The basics
---------------
The draw-module consists of classes that can render themselves on the
wrapper of your platform. The whole gui is built-up tree-like and
reacts on events (Even rendering is an event). The
Frame and
Point
classes are utilites to define absolute or relative sizing and positioning
behaviour of a rendered element while those are always relative
to the super-element in the tree. The base class
RenderedObject
does not have any of theese yet. The
View uses a Frame-object
to calculate its absolute region in pixels while rendering. The
Label
class just uses a Point-object because it is uncomfortable to define the
size of a text directly. You may access the last rendered region (the
real one calculated at rendertime) through
RenderedObject.lastRegion
which is a tuple of 4 numeric values. (The may be int
or float)
The
RenderedObject.lastRegionWh attribute returns the same values
but returns the width and height of the view in the last two elements of
the tuple instead of the position of the bottom-right corner.
Every object needs a
Master to orientate itself. This master will
be the same for every object. You may subclass it in order to keep track
of the GUI elements and organize the whole userinterface.
Also, we need an object that will do the drawing-operations which should
be a subclass of
draw.interface.DrawAreaInterface. In this documentation
you surely want to see some examples and images so I use Cinema 4D
(the built-in implementation) to do this. If you are going to wrap
another platform and use this, only creating the DrawArea and the way
to show it in a dialog (or alike) will differ.
The Cinema 4D implementation is automatically imported when your are
within the program, thats why we access it directly from the module.
Note that the default color of the DrawAreaInterface might differ from
implementation to implementation, in this case its the default color
of Cinema 4D.
import draw
def main():
# the master object, you may subclass it in order to
# organize your gui
master = draw.Master()
# the DrawAreaInterface-subclass object that will
# do the drawing-operations
area = draw.C4dDrawArea(master)
area.minSize = 200, 130
# the c4d-dialog that shows the drawarea
dlg = draw.C4dQuickDialog('A simple gray field.', area)
dlg.Open()
main()
The dialog will look like this:
Quite boring though. Let's make this more interesting. We'll add a
View
object that has another color than the drawarea and that is a little bit offset
from its origin. It will have half the size.
view1 = draw.View(master, x = 10, y = 10, w = 0.5, h = 0.5,
absX = True, absY = True,
color = [.20, .42, .93])
area.addChild(view1)
The y and x values are set to be absolute but the w and h values
to be relative because we left their mode-definition out.
The view will now be offset and
always stay at 10|10 pixels
but it resizes relative to the drawarea.
Note that we have set its width and height to be
exactly the half of the
size of the drawarea. This means the view overlaps about 10 pixels each from the
real middle of the area. In order to show this, we will add another View
with yet another color whoose origin is at 0|0 and also has the half size.
We will see the overlap of the blue view.
view2 = draw.View(master, 0, 0, 0.5, 0.5,
color = [.6, .3, .1])
area.addChild(view2)
area.bringChildToFront(view1)
Note that the
x, y, w, and
h arguments come right after
the master has been specified so you may not specify them as keyword-args.
You can still see a little border in the dialog, that is because the
drawarea has got a padding in it. I can resize
the dialog in any way, the blue view will always overlap the brown one
about 10 pixels.
But what if we want the offset but not the overlap ? That's when modifiers
come in play. There are currently two built-in modifers:
the
ShrinkModifer and the
AlignmentModifer.
view1.addModifier( draw.modifiers.ShrinkModifier(0, 0, 10, 10) )
This will shrink the blue view about 10 pixels on the right-bottom side,
and, vois-là, perfectly aligned.
Reacting on mouse input
---------------------------------
You may ask what has this to do with user-interfaces ? The answer is,
yet, not much. ;) But be patient. Let's say you want to react on a mouseinput
in a view you defined. And let's say you want to create a new view (a white dot
for example) at the place the user clicked. You can either subclass the View or
give the View object a function as value for the
View.mouseEventTarget
attribute. I will demonstrate the latter case here:
import draw
def mouseEvent(view, mouseLocal, event):
# check if the event takes place in this view
# otherwise we don't want to react on the input
if event in view:
x, y = mouseLocal
# create the white dot
# note that you can access the master through an attribute
# of the object
newView = draw.View(view.master, x, y, 2, 2,
absX = True, absY = True, absW = True, absH = True,
color = [1, 1, 1])
# center the newView with an AlignmentModifier
newView.addModifier( draw.modifiers.AlignmentModifier() )
# and make it a child of the original view
view.addChild(newView)
# notify the area to redraw after the event chain
event.area.redrawMessage()
def main():
# same procedure
master = draw.Master()
area = draw.C4dDrawArea(master)
dlg = draw.C4dQuickDialog('Test', area)
view = draw.View(master, 0, 0, 0.8, 0.5, color = [.4, .6, .6])
view.mouseEventTarget = mouseEvent
area.addChild(view)
dlg.Open()
main()
After doing some clicking into the dialog, it may look like this:
But when we resize the dialog, the white dots stay where they are.
That is definitely not what we want, do we ?
We have two choices:
- Defining the position of the dots relative to the view
- Clipping the view
The latter means that the dots do also stay where they are, but you will
no longer see them when outside the blue(-ish ?) view. This is pretty easy:
But let's say we want the position of the dots to be relative. We
need to change something in the
mouseEvent function:
def mouseEvent(view, mouseLocal, event):
if event in view:
# new: obtain the last region(in pixels) the view was rendered with
viewX, viewY, viewWidth, viewHeight = view.lastRegionWh
x, y = mouseLocal
# new: calculate the relative position of the white dots
x = x / float(viewWidth)
y = y / float(viewHeight)
# changed:
newView = draw.View(view.master, x, y, 2, 2,
absX = False, absY = False, absW = True, absH = True,
color = [1, 1, 1])
# ...
Now the dots will move when resizing the dialog.
Note that you can also check for the type of mouse-button invoked the event,
in the example above any type of mouse input will create a white dot.
def mouseEvent(view, mouseLoca, event):
if event in view:
if event.id == draw.MOUSE_LEFT:
# only react on the left mouse
Layouts
----------
Ok, so that was pretty basic. Let's make it more complex. What about
a layout that will show a view on the left, with a fixed width, and
a view to the right that fills the rest of the space. We could easily
build it by giving the right view an offset and giving it a shrink-modifer:
import draw
def main():
master = draw.Master()
area = draw.C4dDrawArea(master)
dlg = draw.C4dQuickDialog('Layout', area)
view_left = draw.View(master, 0, 0, 50, 1, absW = True,
color = [.3, .5, .7], border = [.8, .8, .8])
view_right = draw.View(master, 50, 0, 1, 1, absX = True,
color = [.6, .3, .1])
# shrink the right view about 50 pixels, otherwise it would overlap
# outside the drawarea
view_right.addModifier( draw.modifiers.ShrinkModifier(0, 0, 50, 0) )
area.extendChildren( [view_left, view_right] )
dlg.Open()
main()
But this can become horrible to organize when there are multiple views
with a relative size, etc. ! And adding well configured modifiers for
each is not the one of the yellow. For this case, the
HorizontalLayout
and
VerticalLayout have been implemented. They are easy to use.
What we have to do is giving the views the
View.layoutData attribute.
And also, a layout-object will calculate the region for a View directly and
transfers it to the view beeing rendered. So the view-objects in a
layout will need a sizeing of
x = 0, y = 0, w = 1, h = 1 to fill
the whole area (Theese are the default values in the constructor, so we
leave them out).
For better demonstration purpose, I've added another view to the right.
import draw
def main():
master = draw.Master()
area = draw.C4dDrawArea(master)
dlg = draw.C4dQuickDialog('Layout', area)
view_left = draw.View(master, color = [.3, .5, .7], border = [.8, .8, .8],
layoutData = draw.LayoutData(50, absV = True) )
view_right1 = draw.View(master, color = [.6, .3, .1],
layoutData = draw.LayoutData(1) )
view_right2 = draw.View(master, color = [.4, .8, .6],
layoutData = draw.LayoutData(1) )
# create a layout, it will have a sizing of x = 0, y = 0, w = 1, h = 1
layout = draw.HorizontalLayout(master)
layout.put(view_left)
layout.put(view_right1)
layout.put(view_right2)
area.addChild(layout)
dlg.Open()
main()
See how the two relative views share the same size while the left one
keeps its width. The
draw.LayoutData constructor takes two values:
first the actual value for the layout and then if the value is relative or
absolute. The relative values in this case differ from the usual ones,
because their relations are calculated. So, if one view has got a value of 1
in the layoutData and another has got a value of 2,
the latter will have twice the size than the first !
Animations
---------------
What about animations ? Wouldn't it be fantastic if the user clicks on a
field and then it suddenly resizes ? We will now create a view that will
grow when the user scrolls the mousewheel down and shrink if he scrolls the mousewheel up.
This example is quite mor complex, but you should understand it when taking
a deep look into it.
import draw
class CoolView(draw.View):
FPS = 30.0
DURATION = 0.5
SIZE_MIN = 30
SIZE_MAX = 300
SIZE_STEP = 20
def onInit(self):
self.curThread = None
self.sizeAim = self.frame.w
def resize(self, step, event):
frameCount = self.FPS * self.DURATION
sizeStart = self.frame.w
self.sizeAim += step
if self.sizeAim < self.SIZE_MIN:
self.sizeAim = self.SIZE_MIN
elif self.sizeAim > self.SIZE_MAX:
self.sizeAim = self.SIZE_MAX
if sizeStart == self.sizeAim:
# dont react if the equal
return
def resizeAnimation(frame):
# the thing that does the resizing
x = frame / frameCount
self.frame.w = draw.animation.iSine(x, sizeStart, self.sizeAim)
event.area.redrawNow(True) # redraw from another thread
if self.curThread \
and self.curThread.isAlive():
self.curThread.stop()
self.curThread = draw.threads.Animator(resizeAnimation, self.DURATION, self.FPS)
self.curThread.start()
def mouseEvent(self, mLocal, event):
if event in self:
step = self.SIZE_STEP
if event.id == draw.MOUSE_WHEELUP:
step = -step
elif event.id == draw.MOUSE_WHEELDOWN:
pass
else:
return
self.resize(step, event)
def main():
master = draw.Master()
area = draw.C4dDrawArea(master)
dlg = draw.C4dQuickDialog('', area)
view = CoolView(master, 0, 0, 50, 1, absW = True, color = [.4, .6, .9])
area.addChild(view)
dlg.Open()
main()
1st we subclass the
draw.View class, this allows us to be more
flexible in doing stuff with the view.
In
draw.View.mouseEvent we check if the
event took in place in the view we want. Then we compute the step the view
should increase its size. After this, we call the
CoolView.resize method
that actually creates the thread that will resize our view. Then the size
to start from and the size to end with is computed. The function
resizeAnimation
within
CoolView.resize is called in every frame of the animation. It
calculates the new size (well, actually the width) of the view with an
sine-interpolation and tells the drawarea to redraw.
But we would now hit a problem when just starting a new thread every time
the user scrolls. As multiple threads would now modify the size, you would
see some weird flickering on the screen. That's the reason why we keep track
of the size as an attribute of the view and of the thread. See the animation
below for the result.
Displaying text
----------------------
Well, that's not hard. The built-in
draw.Label class allows you to
do this.
import draw
def main():
master = draw.Master()
area = draw.C4dDrawArea(master)
dlg = draw.C4dQuickDialog('Label example', area)
lab = draw.Label(master, 'Label example', 0.5, 0.5, absX = False, absY = False,
fgColor = [.9, .9, .9], alignH = draw.ALIGN_H_CENTER,
alignV = draw.ALIGN_V_CENTER)
area.addChild(lab)
dlg.Open()
main()
Here is also an animation from the
examples/dragview.py that
shows a drawarea with draggable content, such as a text and a green view.
Combining it all
-----------------------
A good user interface will only be possible when combining all the great features
the draw-module provides. The
draw.ListView class for example is a good way to
display a set of data. Combining it with a
draw.ScrollView you, are
able to display more data than the screen allows it by using the possibility
to scroll content. (Note that sliders are not implemented yet, they will
come in the future for sure.)
Here is an example about the lListView. It is built up from
three parts. The
draw.ListView,
draw.ListViewSource and
the
draw.ListViewSection class. The listview is basically just
caring about displaying the rows correctly, while the source feeds it
with the rows to display. The section is a view displayed as a row in the listview.
import draw
# the data we want to display
data = [ 'First',
'Second',
'Third' ]
# the source will feed the listview with data, but
# will additionally manage some stuff with the sections
class Source(draw.ListViewSource):
sections = None
# overwritten from draw.ListViewSource
def getCount(self):
return len(data)
def getSection(self, master, index):
return Section(master, self)
def updateSection(self, index, section):
section.update(index, data[index])
def reloadEnd(self, sections):
self.sections = sections
# new methods
# will be called by the Section class
def gotFocus(self, index, section):
for s, data in self.sections:
if s.hasFocus:
s.removeFocus()
section.obtainFocus()
class Section(draw.ListViewSection):
GRAY_01 = [.34] * 3
GRAY_02 = [.38] * 3
TEXT_FOCUS = [1.0, .66, .09]
TEXT_NORML = [.8] * 3
def __init__(self, master, source):
super(Section, self).__init__(master)
self.source = source
self.index = 0
self.hasFocus = False
label = draw.Label(master, '', 0.5, 0.5, fgColor = self.TEXT_NORML,
alignH = draw.ALIGN_H_CENTER, alignV = draw.ALIGN_V_CENTER)
self.label = label
self.addChild(label)
# new methods
# called by the Source class
def update(self, index, title):
self.index = index
if not index % 2:
self.color = self.GRAY_01
else:
self.color = self.GRAY_02
self.label.text = title
def removeFocus(self):
self.hasFocus = False
self.label.fgColor = self.TEXT_NORML
def obtainFocus(self):
self.hasFocus = True
self.label.fgColor = self.TEXT_FOCUS
# overwritten
def mouseEvent(self, mLocal, event):
if event in self:
self.source.gotFocus(self.index, self)
event.area.redrawMessage()
def c4dmain():
master = draw.Master()
area = draw.C4dDrawArea(master)
dlg = draw.C4dQuickDialog('ListView example', area)
src = Source()
lview = draw.ListView(master, src, 0, 0, 1, 1)
lview.reloadData()
area.addChild(lview)
dlg.Open()
c4dmain()
Yes it is quite a few code, but easy to maintain, overviewable and procedural.
More examples
-------------------
Make sure to check out the examples folder in the downloaded archive. You
will get the gist when taking a look at the snippets.
Thank you for reading. I hope you will enjoy the draw module !
You may contact me under
rosensteinniklas@googlemail.com
to tell me about bugs, feedback, to ask for support or to show me what cool
stuff you have created.
Niklas Rosenstein
rosensteinniklas@googlemail.com
Copyright (c) by Niklas Rosenstein