Laying out DirectGUI elements.

So I wanted to write some simple layout handler classes for the 2D scene graph. For example, a node that automatically arranges all its child nodes in a line, or a grid, etc. It’s not difficult, but it doesn’t seem to be well documented anywhere how to do this with examples. So people might find this useful, and I’m also sort of hoping people will look at it and tell me what I did wrong or could do better.

First, some things I discovered:

OnscreenImage objects can be aligned using the getPos(), setPos() and getTightBounds() methods. (btw, getTightBounds does not appear in the API.)

OnscreenText objects are less amenable, because the getPos() method is annoying. Rather than a Point3 like OnscreenImage, OnscreenText.getPos() returns either a 2-tuple or a 3-tuple of floats depending on whether you specified a pos when initialising the object. No matter, there is DirectLabel and TextNode to use instead.

For DirectGUI objects the getTightBounds() method does not work, it always returns a bunch of zeros. Fortunately the getBounds() method does work. I tested DirectLabel, DirectButton and DirectEntry, and they all seem to work.

So the code I’ve written is for laying out DirectGUI objects. It’s a partial but working and extendable implementation of the theory of packing boxes layout mechanism from GTK+ (which I only glanced at very briefly, but it looked like a nice idea).

Here’s the code:

"""
layouts.py

A partial implementation of the theory of packing boxes (for laying out
objects in two dimensions) from GTK+. This is meant for laying out
DirectGUI objects, but you can pack any NodePath that implements
getBounds() sensibly.

Theory Of Packing Boxes
-----------------------

From GTK+. Boxes are invisible GUI widgets into which you pack other
widgets to achieve a desired layout. In a horizontal box (hbox) objects
are packed from left to right or right to left. In a vertical box (vbox)
objects are packed from top to bottom or bottom to top. You can pack
objects in both directions in the same box at the same time by using the
two different pack methods. Options control whether objects are packed
as tightly possible, or whether the objects are spread out to fill the
amount of space alloted to the box, etc.

Finally, a table is a type of box that manages a number of cells in rows
and columns. Objects are packed into the cells. A single object can
occupy multiple cells. When packing an object, you specify the range of
cells it will occupy by specifying left, right, bottom, and top cells.

What's implemented so far:

* hboxes with packing from left to right.
* vboxes with packing from top to bottom.
* You can pack boxes into boxes in any combination. Except that you
  cannot pack a box into itself.

Todo:

* Support packing in both directions.
* Option to pack objects tightly or space them out to fill a given
  width.
* Option to insert fixed-size margins between objects.
* Vertical alignment option for hboxes and horizontal alignment option
  for vboxes.
* Table box class.

"""
from pandac.PandaModules import *
from direct.gui.DirectGui import *

LEFT, RIGHT, BOTTOM, TOP = 0, 1, 2, 3

class Box(DirectFrame):
    """
    Base class for HBox and VBox. Not meant to be instantiated itself.
    
    """
    
    def __init__(self):

        DirectFrame.__init__(self)

        # We maintain our own list of objects packed into this box,
        # rather than reusing the scene graph. This means we don't have
        # to capture new children that are added to this node in the
        # scene graph using (for example) reparentTo, and it means that
        # users can add children to this node without them being packed
        # in the box.
        self.objects = []
        
        # The getBounds() method of DirectFrame does not return anything
        # useful so we maintain our own bounds.
        self.bounds = [0.0,0.0,0.0,0.0] # left,right,bottom,top

    def pack(self,obj):
        """
        Pack a new object into this box.
        
        Box.pack handles updating the bounds of the box, appending the
        new object to the list of packed objects, and updating the scene
        graph. It calls layout() to allow the new object to be
        positioned, subclasses should override layout to implement
        different layouts.
        
        """
        self.layout(obj)

        # Get the bounds of the new object.
        left,right,bottom,top = obj.getBounds()        
        # Translate them into the coordinate space of this box.
        top_right = Point3(right,0,top)
        bottom_left = Point3(left,0,bottom)
        left = self.getRelativePoint(obj,bottom_left).getX()
        bottom = self.getRelativePoint(obj,bottom_left).getZ()
        right = self.getRelativePoint(obj,top_right).getX()
        top = self.getRelativePoint(obj,top_right).getZ()
        # Update the bounds of this box to encompass the new object.
        if left < self.bounds[LEFT]: self.bounds[LEFT] = left
        if right > self.bounds[RIGHT]: self.bounds[RIGHT] = right
        if bottom < self.bounds[BOTTOM]: self.bounds[BOTTOM] = bottom
        if top > self.bounds[TOP]: self.bounds[TOP] = top

        # Add the object to the list of packed objects.
        self.objects.append(obj)
    
        # Update the scene graph.
        obj.reparentTo(self)

    def layout(self,obj):
        """
        Position a new object that is being packed into this box.
        Subclasses should override this method to implement their
        own layouts.
        
        """
        pass

    def getBounds(self):
        return self.bounds    
        
class HBox(Box):
    """
    A horizontal container. Objects that are packed into this box will
    be layed out along a horizontal line.
    
    """
            
    def layout(self,obj):
        """
        Align the left side of the new object with the right side of
        the last packed object, and align the top of the new object
        with the top of the last packed object.        
                   
        """
        # FIXME: Use Panda's getRelativePoint() to translate between
        # coordinate spaces instead of doing it manually everywhere. 
                
        if self.objects == []:
            # This is the first object to be packed. Align it with this
            # empty box.
            right = self.getBounds()[RIGHT]
            top = self.getBounds()[TOP]
        else:
            # Align the new object with the last object that was packed.
            last = self.objects[-1]
            right = last.getBounds()[RIGHT]
            right *= last.getScale().getX()
            right += last.getPos().getX()
            top = last.getBounds()[TOP]
            top *= last.getScale().getZ()
            top += last.getPos().getZ()

        # Align the left of the new object with `right`.
        left = obj.getBounds()[LEFT]
        left *= obj.getScale().getX()
        left += obj.getPos().getX()
        distance = right - left
        obj.setPos(obj.getPos() + Point3(distance,0,0))

        # Align the top of the new object with `top`.
        t = obj.getBounds()[TOP]
        t *= obj.getScale().getZ()
        t += obj.getPos().getZ()
        distance = top - t
        obj.setPos(obj.getPos() + Point3(0,0,distance))
   
class VBox(Box):
    """
    A vertical container. Objects that are packed into this box will
    be layed out along a vertical line.
    
    """
        
    def layout(self,obj):
        """
        Align the top side of the new object with the bottom side of
        the last packed object, and align the left of the new object
        with the left of the last packed object.        
                   
        """
        # FIXME: Use Panda's getRelativePoint() to translate between
        # coordinate spaces instead of doing it manually everywhere. 
                
        if self.objects == []:
            # This is the first object to be packed. Align it with this
            # empty box.
            bottom = self.getBounds()[BOTTOM]
            left = self.getBounds()[LEFT]
        else:
            # Align the new object with the last object that was packed.
            last = self.objects[-1]
            bottom = last.getBounds()[BOTTOM]
            bottom *= last.getScale().getZ()
            bottom += last.getPos().getZ()
            left = last.getBounds()[LEFT]
            left *= last.getScale().getX()
            left += last.getPos().getX()

        # Align the top of the new object with `bottom`.
        top = obj.getBounds()[TOP]
        top *= obj.getScale().getZ()
        top += obj.getPos().getZ()
        distance = bottom - top
        obj.setPos(obj.getPos() + Point3(0,0,distance))

        # Align the left of the new object with `left`.
        l = obj.getBounds()[LEFT]
        l *= obj.getScale().getX()
        l += obj.getPos().getX()
        distance = left - l
        obj.setPos(obj.getPos() + Point3(distance,0,0))
    
if __name__== '__main__' :
    """
    Run a demo showing some DirectGUI objects packed into vertical and
    horizontal boxes, and boxes packed into boxes.
    
    """  
    import direct.directbase.DirectStart
       
    # Make 3 vboxes, each containing 5 DirectGUI objects, and pack them
    # all into an hbox.
    hbox = HBox()
    hbox.setPos(-1.3,0,0.9)
    for x in range(3):    
        vbox = VBox()
        dl = DirectLabel(text='Hellooooo!',scale=.05)
        vbox.pack(dl)
        de = DirectEntry(initialText='Hello Text!',scale=.07,pos=(-1,0,-.6))
        vbox.pack(de)
        db = DirectButton(text="I'm a button",scale=.07,pos=(.8,0,0))
        vbox.pack(db)
        another_dl = DirectLabel(text='Hello again',scale=.05)
        vbox.pack(another_dl)
        another_de = DirectEntry(initialText='Blorg',scale=.07,pos=(-1,0,-.6))
        vbox.pack(another_de)
        hbox.pack(vbox)    

    # Make 3 hboxes, each containing 5 DirectGUI objects, and pack them
    # all into aa vbox.
    vbox = VBox()
    vbox.setPos(-1.3,0,0)
    for x in range(3):    
        hbox = HBox()
        dl = DirectLabel(text='Hellooooo!',scale=.05)
        hbox.pack(dl)
        de = DirectEntry(initialText='Hello Text!',scale=.07,pos=(-1,0,-.6))
        hbox.pack(de)
        db = DirectButton(text="I'm a button",scale=.07,pos=(.8,0,0))
        hbox.pack(db)
        another_dl = DirectLabel(text='Hello again',scale=.05)
        hbox.pack(another_dl)
        another_de = DirectEntry(initialText='Blorg',scale=.07,pos=(-1,0,-.6))
        hbox.pack(another_de)
        vbox.pack(hbox)    
    
    run()

You can always wrap it to avoid using its class’ getPos :

NodePath(your_OST).getPos()

You should not use getBounds(), since it’s not the outermost bounds, it’s the bounds without the relief. If you take a closer look, you’ll see that the relief overlaps the next object.
Try this to see it clearly :

    from pandac.PandaModules import LineSegs
    def drawBorder(bounds,color):
        LS=LineSegs()
        LS.setColor(*color)
        LS.moveTo(bounds[0],0,bounds[2])
        LS.drawTo(bounds[0],0,bounds[3])
        LS.drawTo(bounds[1],0,bounds[3])
        LS.drawTo(bounds[1],0,bounds[2])
        return LS.create()

    db = DirectButton(text="__a_ ..",scale=.07,pos=(.8,0,.2),pad=(1,1),borderWidth=(1,1))
    boundsWOrelief=db.getBounds()
    # red
    db.attachNewNode(drawBorder(boundsWOrelief,(1,0,0)))

    boundsWithRelief=db.node().getFrame()
    # green
    db.attachNewNode(drawBorder(boundsWithRelief,(0,1,0)))

How do I get the bounds with the relief? From the manual, I would guess it comes from the frameSize property:

but I’ve found that [‘frameSize’] is None unless you specify it yourself, so that’s no help.

The other problem I’m having is figure out how to draw some sort of background for my boxes. Since a box is a DirectFrame, I thought I would just draw the frame. This code works on its own, drawing and resizing a frame:

from pandac.PandaModules import *
from direct.gui.DirectGui import *
import direct.directbase.DirectStart

l = DirectFrame(frameColor=(0,0,1,1),frameSize=(-1,1,-1,1))
print l['frameSize']
l['frameSize'] = (-.5,.5,-.5,.5)
print l['frameSize']

run()

but when I add code to my Box.pack method to update the size of the frame

self['frame'] = self.bounds

I still don’t see the frame. Even if I set the frame color. In fact I’ve tried setting the frame to all different sizes and doing all sorts of things but I can’t seem to see the frame of one of these boxes.

Okay. So I would like to be able to pack more than just DirectGUI objects into my boxes. For example, cards created by the CardMaker class. It turns out that CardMaker.generate() returns a NodePath. It would be good if my box classes worked with NodePaths as the type of object to pack, a nice, general type. Unfortunately, their are some inconsistencies between matching methods of NodePath and DirectGUI objects. (If anyone has the time DirectGUI really ought to be fixed to be more consistent with the rest of Panda.)

NodePath.getBounds() returns a BoundingSphere object. No good for the kind of alignment I want to do.

NodePath.getTightBounds returns (Point3,Point3), which can be used for my alignment methods.

With DirectGUI objects getBounds() returns a list of four floats [left, right, bottom and top] which can be used for alignment but notice that this is inconsistent with the return type of NodePath. getTightBounds() returns (Point3(0,0,0),Point3(0,0,0)). The same type as NodePath but unfortunately the values are useless.

It looks like I might be able to work around this by writing a wrapper class for DirectGUI objects.

    class DirectGuiWrapper(DirectFrame):
        def __init__(self, **args):
            DirectFrame.__init__(self, **args)
            self.initialiseoptions(DirectGuiWrapper)
    
        def getTightBounds(self):
            l,r,b,t = self.getBounds()
            return (Point3(l,0,b),Point3(r,0,t))

Now getTightBounds returns the correct type and the correct values, so it should work. I’ll try it out soon.

chombee, were you sleepy ? It’s already there, see the green line ?
Here we go again :

    boundsWithRelief=db.node().getFrame()
    # green
    db.attachNewNode(drawBorder(boundsWithRelief,(0,1,0)))

A way to distinguish DGUI from NodePath :

def isItDGUI(obj):
    basesNames=[c.__name__[:6] for c in obj.__class__.__bases__]
    isIt='Direct' in basesNames
    print 'is it DGUI ? ',
    return isIt

print isItDGUI(DirectButton(text='xxxxx'))
print isItDGUI(NodePath(''))

or simply :

print type(camera)==NodePath

I rewrote this code to pack NodePaths instead of DirectGUI objects, which ends up simplifying the code a bit, and then wrote a mixin class that alters DirectGUI classes to make them return a correct getTightBounds() like NodePath does so that they can be packed as well. Here’s the result, it needs a little more refactoring since it’s still deriving from DirectFrame which seems pointless now, should derive from NodePath instead which ought to simplify the code further, but it does the job.

Some of my transformations between coordinate spaces are a bit hoary too.

"""
boxes.py

A partial implementation of the theory of packing boxes (for laying out
objects in two dimensions, although it could easily be extended to 3D)
from GTK+. This framework can be used to layout any NodePath or
DirectGui object.

Theory Of Packing Boxes
-----------------------

From GTK+. Boxes are invisible GUI widgets into which you pack other
widgets to achieve a desired layout. In a horizontal box (hbox) objects
are packed from left to right or right to left. In a vertical box (vbox)
objects are packed from top to bottom or bottom to top. You can pack
objects in both directions in the same box at the same time by using the
two different pack methods. Options control whether objects are packed
as tightly possible, or whether the objects are spread out to fill the
amount of space alloted to the box, etc.

Finally, a table is a type of box that manages a number of cells in rows
and columns. Objects are packed into the cells. A single object can
occupy multiple cells. When packing an object, you specify the range of
cells it will occupy by specifying left, right, bottom, and top cells.

What's implemented so far:

* hboxes with packing from left to right.
* vboxes with packing from top to bottom.
* You can pack boxes into boxes in any combination. Except that you
  cannot pack a box into itself.

Todo:

* Support packing in both directions.
* Option to pack objects tightly or space them out to fill a given
  width.
* Vertical alignment option for hboxes and horizontal alignment option
  for vboxes.
* Table box class.
* Refactor. Box should extend NodePath rather than DirectFrame, and
  should emulate a Python container class.

"""
from pandac.PandaModules import *
from direct.gui.DirectGui import *

# We want our layout containers to be able to contain DirectGUI
# objects as well as NodePaths. The problem: we rely on
# NodePath.getTightBounds(), which returns the bounds of the object
# relative to its parent node. But with DirectGUI objects,
# getTightBounds() just returns (Point3(0,0,0),Point3(0,0,0). So
# we would like to write a wrapper class for DirectGUI objects, but
# which class do we extend? We want a class that is able to extend
# any DirectGUI object. We need to use mixins!
#
# (A mixin is a class that derives from an interface or abstract class,
# so that at run time it may extend any concrete class that implements
# a particular interface. Since Python has no explicit support for
# either interfaces or abstract classes, a Python implementation of
# mixins is a bit more informal than one in Java or C.)
#
def mixin(pyClass, mixInClass):
    """Append mixInClass to the front of the inheritance path of
    pyClass. This alters the behaviour of pyClass at runtime.
        
    This little function implements mixins in Python by adding a
    new class into the inheritance path of a class at runtime. Because
    mixInClass is added to the front of the inheritance path, methods in
    mixInClass will override methods in any other class that pyClass
    derives from (but they will not override methods in pyClass itself,
    although you could also implement mixins like that if you wanted).
    
    """
    if mixInClass not in pyClass.__bases__:
        pyClass.__bases__ = (mixInClass,) + pyClass.__bases__

class Mixin_DirectGuiFix():
    """A class for mixing-in with DirectGUI classes, that overrides
    the behaviour of getTightBounds() to make it consistent with
    NodePath.
    
    To support mixing with this class, a class must have a getBounds()
    method that returns four floats (l,r,b,t), the bounds of the object
    in the objects own coordinate space. All the DirectGUI widgets
    implement this interface.
    
    Since getTightBounds() is useless as is (it always returns a bunch
    of zeros), it should not cause any problem to override its behaviour
    at the class level at runtime. (Surely no one could be making use of
    an apparently useless method?)
    
    """
    def getTightBounds(self):
        # With DirectGUI classes, getBounds() returns four floats,
        # apparently in the coordinate space of the object itself. We
        # translate this into the (Point3,Point3) format and translate
        # the values into the coordinate space of the parent.
        # FIXME: This is not the correct way to translate between
        # coordinate spaces!
        l,r,b,t = self.getBounds()
        l *= self.getScale().getX()
        l += self.getPos().getX()
        r *= self.getScale().getX()
        r += self.getPos().getX()
        b *= self.getScale().getZ()
        b += self.getPos().getZ()
        t *= self.getScale().getZ()
        t += self.getPos().getZ()
        return (Point3(l,0,b),Point3(r,0,t))

# Apply our mixin class to a few DirectGui classes that we will use
# later. You need to do this for every DirectGui class you're gonna use,
# so should probably do it for all of them right here.
mixin(DirectLabel,Mixin_DirectGuiFix)
mixin(DirectEntry,Mixin_DirectGuiFix)
mixin(DirectButton,Mixin_DirectGuiFix)

# FIXME: I don't see any point in Box inheriting from DirectFrame
# anymore and it's starting to get in the way (e.g. Box can't emulate a
# Python container class because DirectFrame is already abusing those
# methods). Should inherit from NodePath instead to get methods like
# setPos, getPos, getTightBounds, reparentTo, etc. that Box uses, but
# this will require some refactoring work. (In theory Box shouldn't need
# to maintain its own bounds as NodePath does this correctly.)
class Box(DirectFrame):
    """
    Base class for HBox and VBox. Not meant to be instantiated itself.
    
    """
    
    def __init__(self,**args):
        """Initialise the Box. **args is just passed to the DirectFrame
        initialiser.
        
        """
        DirectFrame.__init__(self, **args)
        self.initialiseoptions(Box)

        # We maintain our own list of objects packed into this box,
        # rather than reusing the scene graph. This means we don't have
        # to capture new children that are added to this node in the
        # scene graph using (for example) reparentTo, and it means that
        # users can add children to this node without them being packed
        # into the box, which might be useful.
        self._objects = []
        
        # DirectFrame does not appear to maintain its bounds in any
        # useful way  so we maintain our own bounds in the same
        # format used by NodePath.
        self._tightBounds = (Point3(0,0,0),Point3(0,0,0))

    # Some convenience methods for dealing with this annoying bounds
    # structure. This is a humane API!
    def bottom_left(self):
        return self._tightBounds[0]        
    def top_right(self):
        return self._tightBounds[1]
    def left(self):
        return self.bottom_left().getX()    
    def right(self):
        return self.top_right().getX()    
    def bottom(self):
        return self.bottom_left().getZ()    
    def top(self):
        return self.top_right().getZ()    
    def set_left(self,left):
        self.bottom_left().setX(left)
    def set_right(self,right):
        self.top_right().setX(right)
    def set_bottom(self,bottom):
        self.bottom_left().setZ(bottom)
    def set_top(self,top):
        self.top_right().setZ(top)

    def __len__(self):
        return len(self._objects)
    # We would like to emulate a Python container type but DirectFrame
    # is already using those methods so we stay away from them.    
    def get(self,key):
        return self._objects[key]
    # Todo: implement set(self,key,value), del(self,key) and
    # __iter__(self)

    # This is for compatibility with NodePath behaviour.
    def getTightBounds(self):
        """Return the tight bounds of this object relative to its'
        parent node."""
        
        # FIXME: Err... this is not the correct way to translate to the
        # parent nodes coordinate space. It doesn't even account for
        # scale!
        return (self.bottom_left()+self.getPos(),self.top_right()+self.getPos())

    def pack(self,obj):
        """
        Pack a new object into this box.
        
        Box.pack handles updating the bounds of the box, appending the
        new object to the list of packed objects, and updating the scene
        graph. It calls layout() to allow the new object to be
        positioned, subclasses should override layout to implement
        different layouts.
        
        """
        # First reparent the object to the box in the scene graph, so
        # that values it returns are relative to the box.
        obj.reparentTo(self)
        
        # Give derived classes a chance to position the new object.
        self.layout(obj)

        # Get the bounds of the new object.
        bottom_left,top_right = obj.getTightBounds()
        left = bottom_left.getX()
        right = top_right.getX()
        bottom = bottom_left.getZ()
        top = top_right.getZ()
        # Update the bounds of this box to encompass the new object.
        if left < self.left(): self.set_left(left)
        if right > self.right(): self.set_right(right)
        if bottom < self.bottom(): self.set_bottom(bottom)
        if top > self.top(): self.set_top(top)

        # Add the object to the list of packed objects.
        self._objects.append(obj)
    
    def layout(self,obj):
        """
        Position a new object that is being packed into this box.
        Subclasses should override this method to implement their
        own layouts.
        
        """
        pass
        
class HBox(Box):
    """
    A horizontal container. Objects that are packed into this box will
    be layed out along a horizontal line.
    
    """

    def __init__(self,margin=0,**args):
        """Initialise the hbox. margin specifies a horizontal gap
        between each object and the next in the box. **args is passed
        straight to Box.
        
        """
        self.margin = margin
        Box.__init__(self,**args)
            
    def layout(self,obj):
        """
        Align the left side of the new object with the right side of
        the last packed object, and align the top of the new object
        with the top of the last packed object.        
                   
        """             
        if self._objects == []:
            # This is the first object to be packed. Align it with
            # this empty box.
            right = self.right()
            top = self.top()
        else:
            # Align the new object with the last object that was
            # packed.
            last = self._objects[-1]
            bottom_left,top_right = last.getTightBounds()
            right = top_right.getX()
            top = top_right.getZ()

        # Align the left of the new object with `right`.
        bottom_left,top_right = obj.getTightBounds()        
        left = bottom_left.getX()
        distance = right - left
        obj.setPos(obj.getPos() + Point3(distance,0,0))
        
        # Align the top of the new object with `top`.
        t = top_right.getZ()
        distance = top - t
        obj.setPos(obj.getPos() + Point3(0,0,distance))
        
        obj.setPos(obj.getPos() + Point3(self.margin,0,0))
        
class VBox(Box):
    """
    A vertical container. Objects that are packed into this box will
    be layed out along a vertical line.
    
    """

    def __init__(self,margin=0,**args):
        """Initialise the vbox. margin specifies a vertical gap
        between each object and the next in the box. **args is passed
        straight to Box.
        
        """
        self.margin = margin
        Box.__init__(self,**args)
                
    def layout(self,obj):
        """
        Align the top side of the new object with the bottom side of
        the last packed object, and align the left of the new object
        with the left of the last packed object.        
                   
        """
        if self._objects == []:
            # This is the first object to be packed. Align it with
            # this empty box.
            bottom = self.bottom()
            left = self.left()
        else:
            # Align the new object with the last object that was
            # packed.
            last = self._objects[-1]
            bottom_left,top_right = last.getTightBounds()
            bottom = bottom_left.getZ()
            left = bottom_left.getX()

        # Align the top of the new object with `bottom`.
        bottom_left,top_right = obj.getTightBounds()        
        top = top_right.getZ()
        distance = bottom - top
        obj.setPos(obj.getPos() + Point3(0,0,distance))

        # Align the left of the new object with `left`.
        l = bottom_left.getX()
        distance = left - l
        obj.setPos(obj.getPos() + Point3(distance,0,0))
        
        obj.setPos(obj.getPos() + Point3(0,0,-self.margin))
    
if __name__== '__main__' :
    """
    For the test we make a grid of different-coloured cards by packing 4
    cards each into 6 vboxes, and packing the vboxes into an hbox. We
    also put an hbox on each card and pack some DirectGUI objects into
    it.
    
    """      
    import direct.directbase.DirectStart
    from random import random

    # Use the CardMaker to generate some nodepaths for flat,
    # card-like geometry.
    cm = CardMaker('cm')
    left,right,bottom,top = 0,2,0,-2
    width = right - left
    height = top - bottom
    cm.setFrame(left,right,bottom,top)

    hbox = HBox(margin=.05)
    hbox.setPos(-1.2,0,0.9)
    for i in range(5):        
        vbox = VBox(margin=.05)
        for j in range(4):       
            np = aspect2d.attachNewNode(cm.generate())
            np.setScale(.2)
            np.setColor(random(),random(),random())
            another_vbox = VBox(margin=.05)
            dl = DirectLabel(text="I'm a label, look at me!",scale=.2)
            another_vbox.pack(dl)
            de = DirectEntry(initialText="I'm a text entry, write on me!",scale=.2,width=4,numLines=4)
            another_vbox.pack(de)
            db = DirectButton(text="I'm a button, click me!",scale=.2,relief=None)
            another_vbox.pack(db)
            another_vbox.reparentTo(np)
            vbox.pack(np)        
        hbox.pack(vbox)
         
    run()

I’m thinking of using chombee’s code above in a project, basically as surrogate for the more automated layout systems that I’m used to working with in Java’s Swing. Has anyone done any more work on it than what is posted above? Are there other systems I should be seriously considering before diving in with boxes.py? I’m much more interested in getting a nicely laid-out and functional 2d GUI over a panda3d scene than I am in getting the directGUI objects themselves to look really pretty.

Hi mcstrother,

It’s been a while but I think the code I posted in this thread was probably an early version of something that ended up in my ZUI for Panda3D. The links to the code downloads in that thread were broken, so I’ve just hosted the latest PandaZUI code on my github account:

github.com/seanh/PandaZUI/

I am going to cast a level 5 thread necromancy spell here in case anyone familiar with DirectGUI can help me out.

I’m getting odd behaviour when attempting to pack a DirectSlider or DirectScrollBar into an HBox. It seems that the packing “resets” the position of following items.

When adding the slider/scroll in between the two direct labels the position of the second label is reset.