Hiding ScrolledWindow Scrollbars and Avoiding Children Layout Issues

I have a follow up question to this thread: Hiding scroll bars causes rendering to change (Possible bug?)

For my case, I have a ScrolledWindow with buttons where I hide the ScrollBars. I have 2 buttons to scroll left and right.

komoto48g gave an excellent answer and example for updating the client area of the ScrolledWindow after scrolling. Now I am able to scroll in either direction.

However, when I re-size the window, the child buttons’ layout does not update correctly.
Follow these steps to re-create the issue:

  1. Scroll to the right (sizer is no longer at position 0)
  2. Re-size the window (child buttons to the right are re-drawn on top of others)

No issue scrolling right:
no_issues

Issue after window re-size:
issues

I can’t figure out how or where to update the ScrolledWindow’s layout to avoid this. Maybe in a size event handler?

You will notice that I have bound each button to a size event where they are “re-fitted”. This avoids them looking squished when the window is shrunk.

import wx


class AppWindow(wx.Frame):
    def __init__(self, parent, title):
        super(AppWindow, self).__init__(parent=parent, title=title, size=(350, 200))
        # Sizer for the frame
        frame_sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(frame_sizer)

        # Scrolled window for the buttons
        self.scrolled_win = wx.ScrolledWindow(parent=self)
        frame_sizer.Add(self.scrolled_win, proportion=1, flag=wx.EXPAND)
        self.sizer_scrolled_win = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.scrolled_win.SetSizer(sizer=self.sizer_scrolled_win)

        # Add Buttons
        for i in range(1, 8):
            btn = wx.Button(parent=self.scrolled_win, label="Button %s" % i)
            self.sizer_scrolled_win.Add(btn, proportion=1, flag=wx.ALL, border=5)
            btn.Bind(event=wx.EVT_SIZE, handler=self.__on_size_btn)

        # Setup Scrollbars (hidden)
        self.scrolled_win.SetScrollbars(pixelsPerUnitX=10, pixelsPerUnitY=0, noUnitsX=10, noUnitsY=0,
                                        xPos=0, yPos=0, noRefresh=False)
        self.scrolled_win.ShowScrollbars(horz=wx.SHOW_SB_NEVER, vert=wx.SHOW_SB_NEVER)

        # Scroll Buttons
        # - Sizer
        sizer_scroll_btns = wx.BoxSizer(orient=wx.HORIZONTAL)
        frame_sizer.Add(sizer_scroll_btns, proportion=0, flag=wx.EXPAND)
        # - Scroll Left
        scroll_left = wx.Button(parent=self, label="< Left", name='scroll_left')
        sizer_scroll_btns.Add(scroll_left, proportion=0, flag=wx.EXPAND | wx.ALL, border=5)
        scroll_left.Bind(event=wx.EVT_BUTTON, handler=self.__on_button)
        # - Scroll Right
        scroll_right = wx.Button(parent=self, label="Right >", name='scroll_right')
        sizer_scroll_btns.Add(scroll_right, proportion=0, flag=wx.EXPAND | wx.ALL, border=5)
        scroll_right.Bind(event=wx.EVT_BUTTON, handler=self.__on_button)

        self.Show()

    def __on_button(self, event):
        x, y = self.scrolled_win.GetViewStart()

        if event.EventObject.GetName() == 'scroll_right':
            # Scroll buttons right
            self.scrolled_win.Scroll(x=x+10, y=0)

        elif event.EventObject.GetName() == 'scroll_left':
            # Scroll buttons left
            self.scrolled_win.Scroll(x=x-10, y=0)

        # Send size event and update sizer dimension to refresh the scrolled window client area
        self.scrolled_win.SendSizeEvent()
        self.sizer_scrolled_win.SetDimension(pos=self.sizer_scrolled_win.Position,
                                             size=self.sizer_scrolled_win.MinSize)
        event.Skip()

    def __on_size_btn(self, event):
        # Keeps the buttons from being "squished" on re-size
        event.EventObject.Fit()
        event.Skip()


if __name__ == '__main__':
    app = wx.App(False)
    frame = AppWindow(None, "Test")
    app.MainLoop()

Hi shey,

You can work around the sizing issue by adding the following code to AppWindow.__init__,

        def resize(v):
            sizer = self.scrolled_win.Sizer
            wx.CallAfter(sizer.SetDimension,
                         sizer.Position, sizer.MinSize)
            v.Skip()
        self.Bind(wx.EVT_SIZE, resize)

though, it will cause a bit of flickering (tested with wx 4.1.1 Windows 10).
Calling the Scroll method when the scrollbar isn’t visible seems to cause problems such as broken layouts and I’m negative about using it. :worried:

1 Like

Thank you for the quick response. I applied your solution, and you’re right about the flicker.

After some random testing, I found the culprit for the layout/flicker issues: wx.BoxSizer. Using your solution and changing the BoxSizer to a wx.GridBagSizer fixed everything! I would assume any subclass of wx.GridSizer works, but I didn’t test any.

Not sure why this is the case other than the GridSizer class must re-calculate layout after resizing differently. The CallAfter in the size event is still needed to allow scrolling all the way to the left in some cases. Also, I was able to remove the size event binds from each button since “squishing” doesn’t occur with a GridSizer.

Here’s the completed working example…

import wx


class AppWindow(wx.Frame):
    def __init__(self, parent, title):
        super(AppWindow, self).__init__(parent=parent, title=title, size=(350, 200))
        # Sizer for the frame
        frame_sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(frame_sizer)

        # Scrolled window for the buttons
        self.scrolled_win = wx.ScrolledWindow(parent=self)
        frame_sizer.Add(self.scrolled_win, proportion=1, flag=wx.EXPAND)

        # --- Using a BoxSizer results in flicker and bad layout after window re-sizing
        # self.sizer_scrolled_win = wx.BoxSizer(orient=wx.HORIZONTAL)

        # --- GridBagSizer avoids those problems
        self.sizer_scrolled_win = wx.GridBagSizer(vgap=5, hgap=5)
        self.scrolled_win.SetSizer(sizer=self.sizer_scrolled_win)

        # Add Buttons
        for i in range(1, 8):
            btn = wx.Button(parent=self.scrolled_win, label="Button %s" % i)
            self.sizer_scrolled_win.Add(btn, pos=(0, i-1))

        # Setup Scrollbars (hidden)
        self.scrolled_win.SetScrollbars(pixelsPerUnitX=10, pixelsPerUnitY=0, noUnitsX=10, noUnitsY=0,
                                        xPos=0, yPos=0, noRefresh=False)
        self.scrolled_win.ShowScrollbars(horz=wx.SHOW_SB_NEVER, vert=wx.SHOW_SB_NEVER)

        # Scroll Buttons
        # - Sizer
        sizer_scroll_btns = wx.BoxSizer(orient=wx.HORIZONTAL)
        frame_sizer.Add(sizer_scroll_btns, proportion=0, flag=wx.EXPAND)
        # - Scroll Left
        scroll_left = wx.Button(parent=self, label="< Left", name='scroll_left')
        sizer_scroll_btns.Add(scroll_left, proportion=0, flag=wx.EXPAND | wx.ALL, border=5)
        scroll_left.Bind(event=wx.EVT_BUTTON, handler=self.__on_button)
        # - Scroll Right
        scroll_right = wx.Button(parent=self, label="Right >", name='scroll_right')
        sizer_scroll_btns.Add(scroll_right, proportion=0, flag=wx.EXPAND | wx.ALL, border=5)
        scroll_right.Bind(event=wx.EVT_BUTTON, handler=self.__on_button)

        self.Bind(event=wx.EVT_SIZE, handler=self.__on_size)

        self.Show()

    def __on_size(self, event):
        # After a sizing event occurs, reset the sizer dimensions again
        wx.CallAfter(self.sizer_scrolled_win.SetDimension, pos=self.sizer_scrolled_win.Position,
                     size=self.sizer_scrolled_win.MinSize)
        event.Skip()

    def __on_button(self, event):
        x, y = self.scrolled_win.GetViewStart()

        if event.EventObject.GetName() == 'scroll_right':
            # Scroll buttons right
            self.scrolled_win.Scroll(x=x+3, y=0)

        elif event.EventObject.GetName() == 'scroll_left':
            # Scroll buttons left
            self.scrolled_win.Scroll(x=x-3, y=0)

        # Send size event and update sizer dimension to refresh the scrolled window client area
        self.scrolled_win.SendSizeEvent()
        self.sizer_scrolled_win.SetDimension(pos=self.sizer_scrolled_win.Position,
                                             size=self.sizer_scrolled_win.MinSize)
        event.Skip()


if __name__ == '__main__':
    app = wx.App(False)
    frame = AppWindow(None, "Test")
    app.MainLoop()

1 Like

Interesting! :+1: GridBagSizer works well.
I tried the subclasses of it. GridFlexSizer shows no flicker too, but GridSizer does show flicker.

>>> pprint(wx.GridBagSizer.mro())
[<class 'wx._core.GridBagSizer'>,
 <class 'wx._core.FlexGridSizer'>,
 <class 'wx._core.GridSizer'>,
 <class 'wx._core.Sizer'>,
 <class 'wx._core.Object'>,
 <class 'sip.wrapper'>,
 <class 'sip.simplewrapper'>,
 <class 'object'>]