Scrolling Scrollers via the Mouse-wheel

It can be quite convenient to move around scrollable areas via the mouse-wheel. And indeed, this can be achieved via binding a given widget’s wheel-events to a custom callback.

However, as items are added to a given scrollable area, DirectGUI starts to impede this approach: each widget may block events from being passed on, meaning that one may end up binding the wheel-events of nearly each and every widget in order to keep things working.

And with a simple-enough menu, this may not be a problem. With a more-complex one, it may become tiresome, and perhaps even mistake-prone.

Hence the following method (which was developed with aid from the forum).

In short, it uses a ButtonThrower to detect wheel-events, upon which it examines the scrolled-frames nominated by the current menu, determines whether the mouse-pointer is within one, and if so, scrolls it.


The code below is comes from from a project of mine, and contains some elements specific to that project. I’ve adapted some elements, omitted others, and just left still more. As a result, some translation to your own projects might be called for.

For one thing, this comes from a static class, hence the use of class-references and the “@staticmethod” keyword. However, this isn’t inherent to the feature being demonstrated, so feel free to implement it elsehow, such as in an instanced class.

Finally, as I said, I’ve omitted a few things specific to my project. These call for filling in before the code will work properly, if I’m not much mistaken.


        MenuController.navigationEventObject = DirectObject()

        MenuController.universalWheelThrower = base.buttonThrowers[0].node()
        # But consider using a custom button-thrower, with a prefix applied, if 
        # doing the above might result in interference with or from other
        # similar code. In my case, this seemed to be called for, indeed.
        MenuController.navigationEventObject.accept("wheel_up-up", MenuController.handleMouseWheel, extraArgs = [-1])
        MenuController.navigationEventObject.accept("wheel_down-up", MenuController.handleMouseWheel, extraArgs = [1])

Detection and response:

    def handleMouseWheel(dir):
        menu = # Get your menu, however you've implemented that
        if menu is not None:

            # If the user is scrolling a scroll-bar, don't try
            # to scroll the scrolled-frame too. However, if you
            # don't have any scroll-bars, or don't scroll them via
            # mouse-wheel, then feel free to omit this.
            region = base.mouseWatcherNode.getOverRegion()
            if region is not None:
                widget = base.aspect2d.find("**/*{0}".format(
                if isinstance(widget.node(), PGSliderBar) or isinstance(widget.getParent().node(), PGSliderBar):

            # Get the mouse-position
            if base.mouseWatcherNode.hasMouse():
                mousePos = base.mouseWatcherNode.getMouse()
                mousePos = # Some stored value, as you deem fit

            # This is expected to be a list (or tuple) of
            # DirectScrolledFrame instances.
            scrollers = menu.currentScrolledFrameList

            foundScroller = None

            # Determine whether any of the scrolled-frames are
            # under the mouse-pointer
            for scroller in scrollers:
                if scroller is not None:
                    # I've found that the DirectGUI "getBounds"
                    # method doesn't always seem to return
                    # useful data. Perhaps there's a better method
                    # to call, but for myself I've implemented my
                    # own. I leave it to the developer to choose
                    # how they go about this.
                    bounds = # Get the bounds of the scroller

                    if mousePos.x > bounds[0] and mousePos.x < bounds[1] and \
                        mousePos.y > bounds[2] and mousePos.y < bounds[3]:
                        foundScroller = scroller

            if foundScroller is not None:
                # This is slightly hacky: if there's a horizontal scroll-bar, 
                # presume that we want to scroll that way. Otherwise,
                # scroll vertically.
                if not foundScroller.horizontalScroll.isHidden():
                    MenuController.handleMouseScroll(foundScroller.horizontalScroll, dir, None)
                    MenuController.handleMouseScroll(foundScroller.verticalScroll, dir, None)

The actual scrolling:

    def handleMouseScroll(obj, dir, data):
        if isinstance(obj, DirectSlider) or isinstance(obj, DirectScrollBar):
            obj.setValue(obj.getValue() + dir*obj["pageSize"])
1 Like