Best way to get accurate size of DirectGui widget

Hi,

In my attempt to create an automatic layout system for DirectGui widgets, I stumbled upon a couple of issues.

This particular one is about retrieving the exact size of a DirectGui object, immediately after its creation.
There is the getBounds method, which does seem to return correct values, but only after at least one frame has been rendered. Before that, it looks like it returns the bounds of the text(node) of that widget, without taking the frame size into account.

Then there is also the guiItem.getFrame method, which seems to do the opposite and completely disregards the text, returning the size of the frame only (which I suppose is quite logical, given the name of the method).
This would give the desired results, but only if the text doesn’t stick out of the frame (e.g. because of text_pos or due to the text being larger than the frame).

Here is some code to reproduce this:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *


class MyApp:

    def __init__(self):

        self.showbase = showbase = ShowBase()
        showbase.task_mgr.do_method_later(1., self.__create_widgets, "create_widgets")
        showbase.run()

    def __create_widgets(self, task):

        gui_root = self.showbase.pixel2d

        title = "Title bar"
        label = DirectLabel(parent=gui_root, text=title, text_align=TextNode.A_center,
#            pos=(400, 0, -100), textMayChange=1, scale=20, relief=DGG.SUNKEN, borderWidth=(.3, .3))
#            pos=(400, 0, -100), textMayChange=1, frameSize=(0, 1, -.1, .1), scale=20, relief=DGG.SUNKEN)
            pos=(400, 0, -100), textMayChange=1, frameSize=(-10, 10, -.5, .5), scale=20, relief=DGG.SUNKEN)
        print("Size label text before render:", label.getBounds())
        print("Size label frame before render:", label.guiItem.getFrame())

        text = "My Button"
        button = DirectButton(parent=gui_root, text=text, borderWidth=(.3, .3), scale=20,
            pos=(400, 0, -300), textMayChange=1)
        print("Size button text before render:", button.getBounds())
        print("Size button frame before render:", button.guiItem.getFrame())

        self.showbase.graphicsEngine.render_frame()
        print("Size label after render:", label.getBounds())
        print("Size button after render:", button.getBounds())


MyApp()

The output is as follows:

Size label text before render: [-1.8624999523162842, 1.975000023841858, -0.11250001192092896, 0.75]
Size label frame before render: LVecBase4f(-10, 10, -0.5, 0.5)
Size button text before render: [-2.3000001907348633, 2.3874998092651367, -0.32499998807907104, 0.7125000357627869]
Size button frame before render: LVecBase4f(-2.6, 2.6875, -0.625, 1.0125)
Size label after render: [-10.0, 10.0, -0.5, 0.75]
Size button after render: [-2.6000001430511475, 2.687499761581421, -0.625, 1.0125000476837158]

So I seem to have two options currently:

  1. render a frame after creating one or more widgets (a bit annoying for the end users of my project, who will have to call some function to initialize the sizes of my custom wrapper class instances, each time they added new widgets);
  2. combine the results from both methods mentioned above; this seems ideal, but then I would like confirmation from the Panda developers that this would indeed work in all cases, without there being any other factors affecting the final size of a widget, not taken into account for those returned values.

I mentioned that I had a method of my own to do this. I don’t know that it will always produce accurate results, but in case it’s useful, here it is:

    def getGUIBounds(obj, forceCalculation = False, relativeTo = None):
        if not forceCalculation and (obj.isHidden() or obj.isEmpty()):
            return [0, 0, 0, 0]

        if relativeTo is None:
            if aspect2d.isAncestorOf(obj):
                relativeTo = aspect2d
            elif pixel2d.isAncestorOf(obj):
                relativeTo = pixel2d
            elif render2d.isAncestorOf(obj):
                relativeTo = render2d
            else:
                relativeTo = aspect2d

        shouldScaleBounds = True

        node = obj.node()
        if isinstance(node, PGItem):
            bounds = [] + [x for x in node.getFrame()]
        else:
            if isinstance(obj, OnscreenImage):
                bounds = obj.getScale()
                bounds = [-bounds[0], bounds[0], -bounds[2], bounds[2]]
                shouldScaleBounds = False
            else:
                minPt = Vec3()
                maxPt = Vec3()
                obj.calcTightBounds(minPt, maxPt)
                bounds = [minPt.x, maxPt.x, minPt.z, maxPt.z]

        if shouldScaleBounds:
            if relativeTo is not None:
                scale = obj.getScale(relativeTo)
            else:
                scale = obj.getScale(aspect2d)
            scaledBounds = [
                bounds[0]*scale[0],
                bounds[1]*scale[0],
                bounds[2]*scale[2],
                bounds[3]*scale[2]
                ]
        else:
            scaledBounds = bounds

        if relativeTo is not None:
            pos = obj.getPos(relativeTo)
        else:
            pos = (0, 0, 0)
        scaledBounds[0] += pos[0]
        scaledBounds[1] += pos[0]
        scaledBounds[2] += pos[2]
        scaledBounds[3] += pos[2]

        if not isinstance(node, PGScrollFrame):
            for child in obj.getChildren():
                b = Common.getGUIBounds(child, forceCalculation)
                scaledBounds[0] = min(scaledBounds[0], b[0])
                scaledBounds[1] = max(scaledBounds[1], b[1])
                scaledBounds[2] = min(scaledBounds[2], b[2])
                scaledBounds[3] = max(scaledBounds[3], b[3])
        return scaledBounds

However, it does rely on “getFrame”, and doesn’t take into account the text-node. Still, perhaps it could be modified to correct those issues!

1 Like

Looks like this won’t work for DirectEntry; the height ends up being too big.

Thanks for that code, but for the moment I’m going to stick with what I find to be working well. The fact that render_frame needs to be called after adding new widgets remains annoying (more so because it produces a visual glitch as the new widgets visibly get stretched and repositioned from one frame to the next), but it does seem to be the most reliable solution thus far.
Perhaps @rdb can comment on whether there is a fix for this or not.

Again, thanks for the code @Thaumaturge, it’s appreciated, but it does seem a bit complex for something that in all honesty should be rather straightforward, and there seems to be an undefined variable in there (Common), so I didn’t really try it out.
That said, it did draw my attention to the fact that I might have to take things like hidden or empty widget nodes into account, and it made me think about perhaps adding support for non-DirectGui widgets like OnscreenImage (of which there doesn’t seem to be a DirectGui equivalent) as well.

Thanks :slight_smile: !

It’s my pleasure, and fair enough! If that code has helped at all, then I’m glad. And if there is a preferable way to do it, I’m interested to learn it myself! :slight_smile:

Oh, right, sorry about that! “Common” is just the class from which this method comes (and in which it’s a static method, specifically). Thus it’s just recursively calling itself.

Well, I’m glad that you have something that works, at least! :slight_smile:

Apparently I got confused while testing, because that was in fact the correct height. It’s getBounds that only takes text height of a DirectEntry into account, even after a call to render_frame.
So I’m going with this solution after all, as it also seems to work with the remaining DirectGuiWidget types.

1 Like