Slow scrolling speed

I’ve got a wx.ScrolledWindow inside a Frame that is AUI managed. The ScrolledWindow contains a GridSizer that formats about 1000 small Panels, each with an image and some text.

Scrolling the window is impractically SLOW. Is there anything I can do to speed this up? I’ve tried reducing the resolution of the images (little effect). I’ve also played with the scroll speed parameter, with little benefit.

Any advice on how to resolve this would be much appreciated.
thanks,
Eric

Can you post a reduced sample that demonstrates the problem? It could be platform dependent, and it would be easier to suss that out if there was an exemplar.

As requested, here is a sample code. This is quite slow with up-to-date OSX on a recent MacBook Pro. I’m running Python 3.7, wx v4.1, “pythonw” and using PyCharm IDE. How can I speed this up? Thanks.

import wx
import wx.aui

class MainController:
    def __init__(self):

        self.imagesFrame = ImagesFrameAbs(None)
        self.imagesFrame.SetSize(wx.Size( 1200,800 ))
        self.DisplayImages()

    def DisplayImages(self):

        for i in range(900):
            tile = TilePanelAbs(self.imagesFrame.ImagesScrolledWindow)
            bitmap = wx.Bitmap()
            bitmap.LoadFile('foo.jpg', wx.BITMAP_TYPE_ANY)
            tile.ImageBitmap.SetBitmap(bitmap)
            tile.ImageBitmap.SetScaleMode(wx.StaticBitmap.Scale_AspectFit)

            sizer = self.imagesFrame.ImagesScrolledWindow.GetSizer()
            sizer.Add(tile, 0, wx.EXPAND)

class ImagesFrameAbs ( wx.Frame ):

    def __init__( self, parent ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"Images", pos = wx.DefaultPosition, size = wx.Size( 871,606 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
        self.m_mgr = wx.aui.AuiManager()
        self.m_mgr.SetManagedWindow( self )
        self.m_mgr.SetFlags(wx.aui.AUI_MGR_DEFAULT)

        self.ImagesScrolledWindow = wx.ScrolledWindow( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.HSCROLL|wx.VSCROLL )
        self.ImagesScrolledWindow.SetScrollRate( 20, 20 )
        self.m_mgr.AddPane( self.ImagesScrolledWindow, wx.aui.AuiPaneInfo() .Center() .PinButton( True ).Dock().Resizable().FloatingSize( wx.DefaultSize ).CentrePane() )

        ImagesGridSizer = wx.GridSizer( 0, 2, 0, 0 )


        self.ImagesScrolledWindow.SetSizer( ImagesGridSizer )
        self.ImagesScrolledWindow.Layout()
        ImagesGridSizer.Fit( self.ImagesScrolledWindow )
        self.m_panel2 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
        self.m_mgr.AddPane( self.m_panel2, wx.aui.AuiPaneInfo() .Left() .PinButton( True ).Dock().Resizable().FloatingSize( wx.DefaultSize ).MinSize( wx.Size( 100,-1 ) ) )

        bSizer4 = wx.BoxSizer( wx.VERTICAL )

        self.m_button2 = wx.Button( self.m_panel2, wx.ID_ANY, u"MyButton", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer4.Add( self.m_button2, 0, wx.ALL, 5 )


        self.m_panel2.SetSizer( bSizer4 )
        self.m_panel2.Layout()
        bSizer4.Fit( self.m_panel2 )

        self.m_mgr.Update()
        self.Centre( wx.BOTH )

    def __del__( self ):
        self.m_mgr.UnInit()


###########################################################################
## Class TilePanelAbs
###########################################################################

class TilePanelAbs ( wx.Panel ):

    def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 291,242 ), style = wx.BORDER_SIMPLE|wx.TAB_TRAVERSAL, name = wx.EmptyString ):
        wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name )

        self.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_CAPTIONTEXT ) )
        self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) )

        bSizer1 = wx.BoxSizer( wx.VERTICAL )

        self.TileImagePanel = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.BORDER_DEFAULT|wx.TAB_TRAVERSAL )
        self.TileImagePanel.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) )

        bSizer3 = wx.BoxSizer( wx.VERTICAL )

        self.ImageBitmap = wx.StaticBitmap( self.TileImagePanel, wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer3.Add( self.ImageBitmap, 0, wx.ALL, 5 )


        self.TileImagePanel.SetSizer( bSizer3 )
        self.TileImagePanel.Layout()
        bSizer3.Fit( self.TileImagePanel )
        bSizer1.Add( self.TileImagePanel, 1, wx.EXPAND |wx.ALL, 5 )

        bSizer2 = wx.BoxSizer( wx.VERTICAL )

        self.TileTextLink = wx.StaticText( self, wx.ID_ANY, u"An image", wx.DefaultPosition, wx.DefaultSize, 0 )
        self.TileTextLink.Wrap( -1 )

        bSizer2.Add( self.TileTextLink, 0, wx.ALL, 5 )

        bSizer1.Add( bSizer2, 1, wx.EXPAND, 5 )


        self.SetSizer( bSizer1 )
        self.Layout()

    def __del__( self ):
        pass

class MyApp(wx.App):

    def OnInit(self):
        self.frame = MainController().imagesFrame
        self.SetTopWindow(self.frame)
        self.frame.Show(True)
        return True

# -------------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

# -------------------------------------------------------------------------------

if __name__ == "__main__":
    main()

In playing around with this, I get the feeling that, for each change of scroll position, wx is having to re-layout all 900 tiles to figure out the area that needs to be drawn on the screen. Hence, very slow.

However, given that all of the tiles are the same size (they are), it should be possible to determine the visible area with a trivial calculation and only update/draw the visible tiles. Is there a way to implement this? Not sure how.

Or, should I just ditch GridSizer and layout all my tiles on a Panel using absolute positioning? Would that help?

Advice would be greatly appreciated, as I am new to wxPython.
Thanks,
Eric

Sizers are for GUI elements, not for data.

Have a look at the list and grid controls. They are not built from sizers. If they were, they would not be suitable for more than a few items.

When you look at e.g. grid there are two kinds of grid: one that holds the data and alternatively a ‘virtual’ grid that requests the data for visible cells only and renders the visible cells only.
See e.g. the demos Grid_MegaExample.py or GridHugeTable.py.

You should implement an image viewer widget that does the size calculations, loads the images on demand and paints them to the screen.
Maybe, you can use a grid control for this.
If Grid_MegaExample has all features you need, use a grid. Otherwise I would recommend to build your own. In the long run, you probably will switch to your own implementation anyway as you may need user interaction that the grid does not support well.

Thanks for the advice - I’ll look into this.

Just curious… would there be any point in trying to modify wxGridSizer to handle this situation? Seems like it would be useful to have it do a “lazy-drawing” when being scrolled. It’s my understanding that all the grid elements are the same size, so the visible rectangle would be easy to calculate. Do you think this would be a worthwhile avenue?

Eric

The lazy-loading needs to be done by you, not the grid sizer.
Lazy-drawing is done by the windowing system anyway.

Even with lazy-loading, you’re still creating many GUI elements.
Filling the grid sizer by custom elements will probably improve the situation, but going back to the grid example: the grid widget is not a grid sizer with one GUI element per cell. That does not scale up well.

Have you tried binding to wx.EVT_SCROLL_CHANGED instead of wx.EVT_SCROLL?