THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 15/08/2011 at 12:13, xxxxxxxx wrote:
draw (DrawApi) - Preview
-----------------------
NOTE: The module is now called draw, not DrawAPI any more. The example code of the examples shown here is from the old prototype. It is now even much more handy to use. I hope to finish it soon.
Imagine you'd need to create a dynamic Graphical User Interface to show a list of items, like Cinema 4Ds Object Manager for example. It would be a pain for you to subclass the GeUserArea class and to all the drawing stuff, because it's so linear and intransparent.
This is the place where DrawAPI comes in place. It's a module providing object-orientated handling of elements for a User Interface.
The most important class is the View. A View defines a rectangular area on the User Area supporting a backgroundcolor, input events and even subviews.
A views position and size is defined by a Frame, which can carry relative or absolute x, y, width and height values, customizeable for each.
A relative position is always calculated to it's masters absolute position and size. This doesn't mean a View with relative Frame-values does need a superview with only absolute ones. The absolute values are calculated at rendertime allowing you to do any relative bahaviour you want.
There is also a special subclass of a GeUserArea named DrawArea which does already implement the rendering of Views, so you don't even need to handle that.
Let's just create a User Interface with a blue blackground and a View with another Subview on it, just like this hierarchy and properties.
The little r's and a's before a number tells you that the value is either relative or absolute.
DrawArea Color blue
\+ - View Position a10, a10 Size r0.5, r0.5 Color red
\+ - + - View Position r0, r0 Size a20, r1 Color yellow
The resulting Interface will look like this:
We can see that the red area is moved about some pixles (exactly 10) from the blue areas origin (top left) in x and y direction.
The red area is also only as half as wide and high as the blue one. (Remember that the view is really moved, so the red area overlaps about 10 pixels from the real middle of the blue area.)
Next thing we see is the yellow area which has it's origin at the same position as the red view, although it's position is 0, 0. That's because it is a subview of the red one and it's position and size is relative to this one. This also leads to that the yellow area is as high as the red one, but is only 20 pixels wide.
And this is the simple code to achieve this.
import c4d
import DrawAPI as d
COLOR_BLUE = c4d.Vector(.3, .4, .8)
COLOR_RED = c4d.Vector(.8, .25, .15)
COLOR_YELLOW= c4d.Vector(.8, .7, .3)
def main() :
area = d.DrawArea()
area.color = COLOR_BLUE # make the DrawArea's backgroundcolor blue
# create a View with our desired properties
view1 = d.View(10, 10, .5, .5, abs_x = True, abs_y = True)
view1.color = COLOR_RED
area.AddSubview(view1) # make view1 a subview of the DrawArea
# create the second View with our desired properties
view2 = d.View(0, 0, 20, 1, abs_w = True)
view2.color = COLOR_YELLOW
view2.AddSubview(view2) # make view2 a subview of the first view
dlg = d.TestDialog(area) # a dialog just showing a GeUserArea
dlg.Open() # by default c4d.DLG_TYPE_MODAL_RESIZEABLE
main()
The code is actually for the Script manager. That is the best way to create and test interfaces for the DrawAPI, because you can get immediate feedback when clicking on execute.
Now, what's with the example from above, the Interface with the list of items ? No problem. DrawAPI comes with a class for easy and efficient creating such, called the ListView.
The ListView does actually nothing more than creating subviews that are the items in the list.
But those Views must be a special subclass of (or provide the same interface as) ListViewRow. It is important that those Views provide a GetHeight() method, otherwise the ListView wouldn't know where to place the next row.
Also, the ListView needs to know how many items to display, etc. This is why it also needs an instance of ListViewSource (or an object that provides the same interface).
Let's just show you an example step-by-step. This will also introduce another class, the Label, which does nothing more than showing text.
The resulting Interface will look like this:
First thing we need to do is to decide if we subclass the ListViewRow to apply our desired behaviour or to just add a Label as subview. I decide to subclass it, as we need to override the ListViewRow.Update() method.
This is actually very simple, because we only need to add a Label to the Row and override the Update method to be able to modify our row's label's text.
import c4d
import DrawAPI as d
COLOR_GREY1 = c4d.Vector(.21)
COLOR_GREY2 = c4d.Vector(.23)
COLOR_WHITE = c4d.Vector(.85)
class TextRow(d.ListViewRow) :
def OnInit(self) :
'Called on initialization to bypass overriding __init__.'
# create a label with (yet) no text and an absolute position of 5, 2
# (we don't want the label to change it's position when the userarea resizes)
# note that the Label-constructor does not take any argument defining the size.
# a label does only have a position.
self.label = d.Label('', 5, 2, abs_x = True, abs_y = True) # text, x, y
self.label.textcolor = COLOR_WHITE # set the textcolor to white
self.AddSubview(self.label) # otherwise the label wouldn't be rendered
# we also want to have our view to be 17 pixels high
# note: you can override GetHeight, but the default implementation does already
# return the value of self.frame.size.y
self.frame.size.y = 17
def Update(self, text, index) :
'''
This method is called in order to update some content within the row.
We can define the arguments in whatever way we want, because the arguments
passed to this method are defined in the ListViewSource.GetUpdateData() method.
We also pass the index of the row to this method, because we want to set the
background color according to this value, so the rows differ from each other.
'''
self.label.text = text
# set backgroundcolor
if index % 2:
self.color = COLOR_GREY1
else:
self.color = COLOR_GREY2
Next thing is to define our source for the ListView.
class TextSource(d.ListViewSource) :
def __init__(self, *strings) :
self._data = strings
def GetCount(self) :
'Called in order to get the number of items in the list.'
return len(self._data)
def MakeRow(self, index) :
'Called in order to recieve a view that is placed in the list.'
# the default constructor of ListViewRow does not take any arguments.
return TextRow()
def GetUpdateData(self, index) :
'''
This method needs to return arguments and keyword-arguments passed
to ListViewRow.Update().
In our case, we need to return the value of the text-argument and
the row's index.
'''
return (self._data[index], index), {} # a tuple with the string and the index and an empty dictionary
Now, the only thing left to do is to create a ListView with our Source, a DrawArea and a Dialog.
def main() :
area = d.DrawArea()
source = TextSource('I am the first row.', 'And I am the second one.', 'Could you please be quiet up there ?')
list_ = d.ListView(source, 0, 0, 1, 1) # the source, x, y, width, height
list_.ReloadData() # must be called, otherwise there won't be any rows in the ListView
area.AddSubview(list_)
dlg = d.TestDialog(area)
dlg.Open()
main()
Easy practice, isn't it ?
Let's make this waaaay cooler ! What about the ability to click on one of the items causing the row the become bigger and revealing new content ?
A View class does implement a MouseEvent and AnyEvent method. You can guess what method is called when.
Overriding those will enable us to achieve the desired behaviour.
class TextRow(d.ListViewRow) :
HEIGHT_DEFAULT = 17
HEIGHT_EXPANDED = 40
# ....
def SetHeight(self, h) :
self.frame.size.y = h
def MouseEvent(self, area, msg, mouse_global, mouse_local) :
'Called whenever the mouse clicks into this view.'
# we use ListViewRow.GetHeight() because it's convenient in this case and we
# don't need to write self.frame.size.y
if self.GetHeight() == self.HEIGHT_DEFAULT:
self.SetHeight(self.HEIGHT_EXPANDED)
else:
self.SetHeight(self.HEIGHT_DEFAULT)
# we need the listview to tell it there was something changed within it's rows
# using ListView.SendReloadMessage(with_update = False) we can do so.
# as we only need to refresh the position of the rows and not to call the rows'
# Update() method, we omit the call_update argument
# Note that we could also call ListView.ReloadData(False), but imagine more than one
# row would call this method, the rows would be unnecessarily updated multiple times,
self.list_view.SendReloadMessage()
# also, we need to tell the area to redraw after that input event.
# same as with ListView.SendReloadMessage(), we don't call UserArea.Redraw()
# directly, because it would lead to highly redundant redraw-traffic if
# more than one row would call it.
area.SendRedrawMessage()
Now that we've implemented the mouseclick, we could add content to be shown on the new area revealed when clicking, but that's too much for this preview of the DrawAPI. But just to whet your appetite, we could add a subview that is within the hidden area of the row and set it's opaque-attribute to False, so it won't be rendered. When the user clicks and MouseEvent is called, we set it to True.
The result when clicking on the first and last row, those are expanded to 40 pixels, while the middle row is still at 17 pixels.
This was (hopefully) an introduction that made you curious about using the DrawAPI.
Please leave some comments below and ask questions if you have any.
The DrawAPI will be released soon as a stable release, ready for you to use. However, it will be extended by time and API might change. There also won't be a documentation, but methods are well commented. (Yes, the module will be Open Source).
Thanks for reading !
Niklas