SOLVED Scaling a GeUserArea in a ScrollGroup

Hello!
I am drawing a BaseBitmap in a GeUserArea that is in a scroll group using the workaround @m_adam employed in this post:
Redrawing GeUserArea in ScrollGroup with Slider Input

The GeUserArea scales properly with this workaround when not it's smaller than the scrollgroup, but I'm unable to get it to stay in the center of the scrollgroup as it becomes larger. The GeUserArea shifts to the top left, which is undesired.

GeUserArea

Code:

import c4d, random
from c4d import gui

GADGET_ID_GEUSERAREA = 10000
SCROLL_ID = 10001
SLIDER_ID = 10002

class ExampleGeUserArea(c4d.gui.GeUserArea):
    width = 400
    height = 500
    bmp_cache = None

    def GetBitmap(self):
        bmp = c4d.bitmaps.BaseBitmap()
        bmp.Init(self.width,self.height)
        for h in range(self.height):
            for w in range(self.width):
                r = random.randint(0, 70)
                bmp.SetPixel(w, h, r, r, r)
        return bmp

    def DrawMsg(self, x1, y1, x2, y2, msg):
        print("DrawMsg: x1: %d, y1: %d, x2: %d, y2: %s"%(x1,y1,x2,y2))
        self.OffScreenOn()
        self.SetClippingRegion(x1, y1, x2, y2)
        self.DrawSetPen(c4d.Vector(1,0,0))
        self.DrawRectangle(x1, y1, x2, y2)
        if self.bmp_cache == None:
            self.bmp_cache = self.GetBitmap()

        self.DrawBitmap(self.bmp_cache, x1, y1, x2, y2, 0, 0,
                        w=self.bmp_cache.GetBw(),
                        h=self.bmp_cache.GetBh(),
                        mode=c4d.BMP_NORMAL)        

    def Message(self, msg, result):
        # Catch the draw message to cancel it (return True)
        # and call ourself the DrawMsg with the dimension we expect
        if msg.GetId() == c4d.BFM_DRAW:
            self.DrawMsg(0, 0, self.width, self.height, c4d.BaseContainer())

            return True
    
        return c4d.gui.GeUserArea.Message(self, msg, result)

    def GetMinSize(self):
        print("GetMinSize: self.width: %d, self.height: %d"%(self.width,self.height))
        return self.width,self.height

class ExampleDialog(c4d.gui.GeDialog):
    geUserArea = ExampleGeUserArea()

    def DrawUA(self,scale):
        print("DrawUserArea", int(400*scale), int(500*scale))
        self.geUserArea.width = int(400*scale)
        self.geUserArea.height = int(500*scale)
        self.LayoutChanged(SCROLL_ID)

    def CreateLayout(self):
        self.SetTitle("GeUserArea")
        if self.ScrollGroupBegin(SCROLL_ID, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, c4d.SCROLLGROUP_HORIZ | c4d.SCROLLGROUP_VERT):
            self.AddUserArea(GADGET_ID_GEUSERAREA, c4d.BFH_CENTER | c4d.BFH_SCALE | c4d.BFV_CENTER | c4d.BFV_SCALE, initw=gui.SizePix(400), inith=gui.SizePix(500))
            self.AttachUserArea(self.geUserArea, GADGET_ID_GEUSERAREA)
        self.GroupEnd()
        self.AddEditSlider(SLIDER_ID, c4d.BFH_SCALEFIT | c4d.BFV_CENTER)
        return True

    def InitValues(self):
        self.SetFloat(SLIDER_ID, 1.0, min = 0.01, max = 2, step = 0.01)
        return True

    def Command(self,id,msg):
        if id == SLIDER_ID:
            self.DrawUA(msg[c4d.BFM_ACTION_VALUE])
        return True

def main():
    global dlg
    dlg = ExampleDialog()
    dlg.Open(c4d.DLG_TYPE_ASYNC, pluginid=1234567, defaultw=300, defaulth=300)


if __name__ == "__main__":
    main()

Is there a way to get this GeUserArea to scale up into the scrollgroup from the center?

Thank you!

Hi @blastframe,

so, this thread has not been forgotten by me 🙂 Yesterday I finally got around to taking a closer look on that problem. I am still not quite sure yet if this a bug or not (I have reached out to the devs for now). There are two problems here at play:

  1. GeDialog.GetItemDim(ID_SOME_SCROLLGROUP) takes the scroll bars of a scroll group into account, GeDialog.SetVisibleArea(ID_SOME_SCROLLGROUP) does not.
  2. GeDialog.SetVisibleArea(ID_SOME_SCROLLGROUP) is behaving differently than advertised in the documentation and will do nothing for frames larger than the scroll group it is being called on.

I did provide a working example for your problem at the end which wiggles itself around these problems. I also reached out to the devs if we want to consider this a bug or not. If we don't, we will update the documentation of at least SetVisibleArea to make this need for being precise about the passed frame more clear. For now the little work around should work fine in your case, it is only that the code might look a bit cryptic.

Cheers,
Ferdinand

"""Example for centering a scroll group.

I have added the method ExampleDialog._centerAlignScrollGroup() and two
lines calling that method. For details read _centerAlignScrollGroup().

As discussed in:
    plugincafe.maxon.net/topic/13205
"""

import c4d
import random
from c4d import gui

GADGET_ID_GEUSERAREA = 10000
SCROLL_ID = 10001
SLIDER_ID = 10002


class ExampleGeUserArea(c4d.gui.GeUserArea):
    width = 400
    height = 500
    bmp_cache = None

    def GetBitmap(self):
        bmp = c4d.bitmaps.BaseBitmap()
        bmp.Init(self.width, self.height)
        for h in range(self.height):
            for w in range(self.width):
                r = random.randint(0, 70)
                bmp.SetPixel(w, h, r, r, r)
        return bmp

    def DrawMsg(self, x1, y1, x2, y2, msg):
        # print("DrawMsg: x1: %d, y1: %d, x2: %d, y2: %s" % (x1, y1, x2, y2))
        self.OffScreenOn()
        self.SetClippingRegion(x1, y1, x2, y2)
        self.DrawSetPen(c4d.Vector(1, 0, 0))
        self.DrawRectangle(x1, y1, x2, y2)
        if self.bmp_cache == None:
            self.bmp_cache = self.GetBitmap()

        self.DrawBitmap(self.bmp_cache, x1, y1, x2, y2, 0, 0,
                        w=self.bmp_cache.GetBw(),
                        h=self.bmp_cache.GetBh(),
                        mode=c4d.BMP_NORMAL)

    def Message(self, msg, result):
        # Catch the draw message to cancel it (return True)
        # and call ourself the DrawMsg with the dimension we expect
        if msg.GetId() == c4d.BFM_DRAW:
            self.DrawMsg(0, 0, self.width, self.height, c4d.BaseContainer())

            return True

        return c4d.gui.GeUserArea.Message(self, msg, result)

    def GetMinSize(self):
        # print("GetMinSize: self.width: %d, self.height: %d" %
        #      (self.width, self.height))
        return self.width, self.height


class ExampleDialog(c4d.gui.GeDialog):
    geUserArea = ExampleGeUserArea()

    def _centerAlignScrollGroup(self):
        """Centers the scroll group.

        There are two problems/bugs here at play which we have to deal with:

            1. GeDialog.GetItemDim(SCROLL_ID) takes the scroll bars of a
               scroll group into account, GeDialog.SetVisibleArea(SCROLL_ID)
               does not.
            2. GeDialog.SetVisibleArea(SCROLL_ID) is behaving differently 
               than advertised in the documentation frames and does nothing
               for frames larger than the scroll group it is being called upon.

        My workaround here simply computes the deltas for the unaccounted 
        scrollbars.
        """
        # The width and height of the scroll group and user area gadgets.
        _, _, sgw, sgh = self.GetItemDim(SCROLL_ID).values()
        _, _, uaw, uah = self.GetItemDim(GADGET_ID_GEUSERAREA).values()

        # These are the magic numbers we have to apply for now after querying
        # a scroll group for its dimensions. These are the scroll bars which
        # are being accounted for in the total values. Which get in the way
        # when we do not deal with them, due to Get/SetVisibleArea not taking
        # them into account.
        # sgw -= 16
        # sgh -= 15

        # To take the safe route, i.e., to make this OS and somewhat version 
        # agnostic, we should compute these deltas ourselves.
        sax1, say1, sax2, say2 = self.GetVisibleArea(SCROLL_ID).values()
        dx, dy = sgw - (sax2 - sax1), sgh - (say2 - say1) # 16, 15
        sgw -= dx
        sgh -= dy

        # When we do not do this, .SetVisibleArea() will fail due not working
        # as advertised. It cannot handle frames larger than the gadget and
        # will carry out something different than instructed with such a too
        # large frame (which in most cases will seem as if it did not do
        # anything).

        # The top left corner of the centered scroll bar frame.
        x, y = int((uaw - sgw) * .5), int((uah - sgh) * .5)
        self.SetVisibleArea(SCROLL_ID, x, y, x + sgw, y + sgh)

    def DrawUA(self, scale):
        # print("DrawUserArea", int(400*scale), int(500*scale))
        self.geUserArea.width = int(400*scale)
        self.geUserArea.height = int(500*scale)
        self.LayoutChanged(SCROLL_ID)

    def CreateLayout(self):
        self.SetTitle("GeUserArea")
        if self.ScrollGroupBegin(SCROLL_ID, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, c4d.SCROLLGROUP_HORIZ | c4d.SCROLLGROUP_VERT):
            self.AddUserArea(GADGET_ID_GEUSERAREA, c4d.BFH_CENTER | c4d.BFH_SCALE |
                             c4d.BFV_CENTER | c4d.BFV_SCALE, initw=gui.SizePix(400), inith=gui.SizePix(500))
            self.AttachUserArea(self.geUserArea, GADGET_ID_GEUSERAREA)
        self.GroupEnd()
        self.AddEditSlider(SLIDER_ID, c4d.BFH_SCALEFIT | c4d.BFV_CENTER)
        return True

    def InitValues(self):
        self.SetFloat(SLIDER_ID, 1.0, min=0.01, max=2, step=0.01)
        # Center the scroll group.
        self._centerAlignScrollGroup()
        return True

    def Command(self, id, msg):
        if id == SLIDER_ID:
            self.DrawUA(msg[c4d.BFM_ACTION_VALUE])
        # Center the scroll group.
        self._centerAlignScrollGroup()
        return True


def main():
    global dlg
    dlg = ExampleDialog()
    dlg.Open(c4d.DLG_TYPE_ASYNC, pluginid=1234567, defaultw=300, defaulth=300)


if __name__ == "__main__":
    main()

Hi @blastframe,

thank you for reaching out to us. Your problem can be solved in principle with GeDialog.GetItemDim(SCROLL_ID) to measure the diemension of the scroll group and GeDialog.SetVisibleArea() to set its, well, visible area 🙂 But in the process of writing the answer to your request I stumbled upon some weird behaviour of SetVisibleArea. I am currently not yet sure if this is being caused by me just not grasping some intricacies of SetVisibleArea or a bug in it. It will take me some time to investigate this. I will come then back with an answer.

Cheers,
Ferdinand

Hi @blastframe,

so, this thread has not been forgotten by me 🙂 Yesterday I finally got around to taking a closer look on that problem. I am still not quite sure yet if this a bug or not (I have reached out to the devs for now). There are two problems here at play:

  1. GeDialog.GetItemDim(ID_SOME_SCROLLGROUP) takes the scroll bars of a scroll group into account, GeDialog.SetVisibleArea(ID_SOME_SCROLLGROUP) does not.
  2. GeDialog.SetVisibleArea(ID_SOME_SCROLLGROUP) is behaving differently than advertised in the documentation and will do nothing for frames larger than the scroll group it is being called on.

I did provide a working example for your problem at the end which wiggles itself around these problems. I also reached out to the devs if we want to consider this a bug or not. If we don't, we will update the documentation of at least SetVisibleArea to make this need for being precise about the passed frame more clear. For now the little work around should work fine in your case, it is only that the code might look a bit cryptic.

Cheers,
Ferdinand

"""Example for centering a scroll group.

I have added the method ExampleDialog._centerAlignScrollGroup() and two
lines calling that method. For details read _centerAlignScrollGroup().

As discussed in:
    plugincafe.maxon.net/topic/13205
"""

import c4d
import random
from c4d import gui

GADGET_ID_GEUSERAREA = 10000
SCROLL_ID = 10001
SLIDER_ID = 10002


class ExampleGeUserArea(c4d.gui.GeUserArea):
    width = 400
    height = 500
    bmp_cache = None

    def GetBitmap(self):
        bmp = c4d.bitmaps.BaseBitmap()
        bmp.Init(self.width, self.height)
        for h in range(self.height):
            for w in range(self.width):
                r = random.randint(0, 70)
                bmp.SetPixel(w, h, r, r, r)
        return bmp

    def DrawMsg(self, x1, y1, x2, y2, msg):
        # print("DrawMsg: x1: %d, y1: %d, x2: %d, y2: %s" % (x1, y1, x2, y2))
        self.OffScreenOn()
        self.SetClippingRegion(x1, y1, x2, y2)
        self.DrawSetPen(c4d.Vector(1, 0, 0))
        self.DrawRectangle(x1, y1, x2, y2)
        if self.bmp_cache == None:
            self.bmp_cache = self.GetBitmap()

        self.DrawBitmap(self.bmp_cache, x1, y1, x2, y2, 0, 0,
                        w=self.bmp_cache.GetBw(),
                        h=self.bmp_cache.GetBh(),
                        mode=c4d.BMP_NORMAL)

    def Message(self, msg, result):
        # Catch the draw message to cancel it (return True)
        # and call ourself the DrawMsg with the dimension we expect
        if msg.GetId() == c4d.BFM_DRAW:
            self.DrawMsg(0, 0, self.width, self.height, c4d.BaseContainer())

            return True

        return c4d.gui.GeUserArea.Message(self, msg, result)

    def GetMinSize(self):
        # print("GetMinSize: self.width: %d, self.height: %d" %
        #      (self.width, self.height))
        return self.width, self.height


class ExampleDialog(c4d.gui.GeDialog):
    geUserArea = ExampleGeUserArea()

    def _centerAlignScrollGroup(self):
        """Centers the scroll group.

        There are two problems/bugs here at play which we have to deal with:

            1. GeDialog.GetItemDim(SCROLL_ID) takes the scroll bars of a
               scroll group into account, GeDialog.SetVisibleArea(SCROLL_ID)
               does not.
            2. GeDialog.SetVisibleArea(SCROLL_ID) is behaving differently 
               than advertised in the documentation frames and does nothing
               for frames larger than the scroll group it is being called upon.

        My workaround here simply computes the deltas for the unaccounted 
        scrollbars.
        """
        # The width and height of the scroll group and user area gadgets.
        _, _, sgw, sgh = self.GetItemDim(SCROLL_ID).values()
        _, _, uaw, uah = self.GetItemDim(GADGET_ID_GEUSERAREA).values()

        # These are the magic numbers we have to apply for now after querying
        # a scroll group for its dimensions. These are the scroll bars which
        # are being accounted for in the total values. Which get in the way
        # when we do not deal with them, due to Get/SetVisibleArea not taking
        # them into account.
        # sgw -= 16
        # sgh -= 15

        # To take the safe route, i.e., to make this OS and somewhat version 
        # agnostic, we should compute these deltas ourselves.
        sax1, say1, sax2, say2 = self.GetVisibleArea(SCROLL_ID).values()
        dx, dy = sgw - (sax2 - sax1), sgh - (say2 - say1) # 16, 15
        sgw -= dx
        sgh -= dy

        # When we do not do this, .SetVisibleArea() will fail due not working
        # as advertised. It cannot handle frames larger than the gadget and
        # will carry out something different than instructed with such a too
        # large frame (which in most cases will seem as if it did not do
        # anything).

        # The top left corner of the centered scroll bar frame.
        x, y = int((uaw - sgw) * .5), int((uah - sgh) * .5)
        self.SetVisibleArea(SCROLL_ID, x, y, x + sgw, y + sgh)

    def DrawUA(self, scale):
        # print("DrawUserArea", int(400*scale), int(500*scale))
        self.geUserArea.width = int(400*scale)
        self.geUserArea.height = int(500*scale)
        self.LayoutChanged(SCROLL_ID)

    def CreateLayout(self):
        self.SetTitle("GeUserArea")
        if self.ScrollGroupBegin(SCROLL_ID, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, c4d.SCROLLGROUP_HORIZ | c4d.SCROLLGROUP_VERT):
            self.AddUserArea(GADGET_ID_GEUSERAREA, c4d.BFH_CENTER | c4d.BFH_SCALE |
                             c4d.BFV_CENTER | c4d.BFV_SCALE, initw=gui.SizePix(400), inith=gui.SizePix(500))
            self.AttachUserArea(self.geUserArea, GADGET_ID_GEUSERAREA)
        self.GroupEnd()
        self.AddEditSlider(SLIDER_ID, c4d.BFH_SCALEFIT | c4d.BFV_CENTER)
        return True

    def InitValues(self):
        self.SetFloat(SLIDER_ID, 1.0, min=0.01, max=2, step=0.01)
        # Center the scroll group.
        self._centerAlignScrollGroup()
        return True

    def Command(self, id, msg):
        if id == SLIDER_ID:
            self.DrawUA(msg[c4d.BFM_ACTION_VALUE])
        # Center the scroll group.
        self._centerAlignScrollGroup()
        return True


def main():
    global dlg
    dlg = ExampleDialog()
    dlg.Open(c4d.DLG_TYPE_ASYNC, pluginid=1234567, defaultw=300, defaulth=300)


if __name__ == "__main__":
    main()

@ferdinand You are an absolute genius. 🏆 Thank you so so much, Ferdinand. Incredible!!

Hello @blastframe,

I forgot to put the final outcome for this here. So, after some back and forth we decided to not fix this for reasons I unfortunately cannot disclose. But we will update the documentation in an upcoming release to avoid this being as opaque as it currently is. This is probably not what you hoped for, but currently the best course of actions for us.

Thank you for your understanding,
Ferdinand