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: 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:
    view.clipRegion = True

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