Search code examples
wxpythonwxwidgets

wxPython/wxWidgets very long scroll area with thumbnails


I am trying to create a vertical list of image thumbnails that could potentially reach hundreds/thousands of images. Is there a recommended strategy in wxPython/wxWidgets that would allow for efficient loading of the list so that all images aren't required to be loaded immediately. On iOS, there is the concept of cell reuse in a UITableView and I wasn't sure if there was anything similar here or if the recommended solution is just to preload all of them. I also considered loading a default image and then loading the images in a separate thread.

Here is a prototype that shows what I'm trying to accomplish (with the red boxes replace with image thumbnails)

enter image description here

Here is the code that I used to generate the prototype:

import wx
from wx.lib.scrolledpanel import ScrolledPanel

eng = wx.App()
frame = wx.Frame(parent=None, title='wxPython')

main_panel = wx.Panel(frame)
main_sizer = wx.BoxSizer(wx.HORIZONTAL)
main_panel.SetSizer(main_sizer)

left_panel = ScrolledPanel(main_panel)
left_panel.SetupScrolling()
left_panel.SetBackgroundColour(wx.Colour(0,255,0))
left_panel_sizer = wx.BoxSizer(wx.VERTICAL)
left_panel.SetSizer(left_panel_sizer)
main_sizer.Add(left_panel, 1, wx.ALL | wx.EXPAND, 0)

right_panel = wx.Panel(main_panel)
right_panel.SetBackgroundColour(wx.Colour(0,0,255))
main_sizer.Add(right_panel, 4, wx.ALL | wx.EXPAND, 0)

for i in range(1000):
    left_panel_sizer.AddSpacer(10)
    thumbnail = wx.Panel(left_panel)
    thumbnail.SetMinSize((175,240))
    thumbnail.SetBackgroundColour(wx.Colour(255,0,0))
    left_panel_sizer.Add(thumbnail, 3, wx.ALL | wx.CENTER)
    left_panel_sizer.AddSpacer(10)


frame.Show()
frame.SetSize(1440,875)
frame.CenterOnScreen()

eng.MainLoop()

Solution

  • Following VZ's suggestion, I realized that manually drawing only the visible thumbnails would be the best solution.

    There were several classes that could accomplish this in wxpython: wx.VListBox, wx.VScrolledWindow, or wx.ScrolledWindow

    wx.VListBox provided the simplest API. You created a custom subclass and implemented OnMeasureItem to specify the cell height and OnDrawItem specify the drawing code just for your item. The downside of this approach was that the scrolling would 'lock' in position at the top of each item. wx.VScrolledWindow had this same issue with locked scroll position with a slightly different API that could be useful in other contexts.

    wx.ScrolledWindow was the most useful since it solved the problem of locked scroll position. But it involved a little calculation to only draw the cells that were in view. Below is the prototype that implements this:

    import wx
    
    class ThumbnailView(wx.ScrolledWindow):
        
        def __init__(self, *args, **kwargs):
            wx.VScrolledWindow.__init__(self, *args, **kwargs)
    
            self.ScrollRate = 10
            self.ThumbnailHeight = 100
            self.NumThumbnails = 100000
    
            self.SetVirtualSize(-1, self.ThumbnailHeight*self.NumThumbnails)
            self.SetScrollRate(0, self.ScrollRate)
    
        def OnDraw(self, dc):
            width, height = self.GetSize()
    
            # Calculate min and max thumbnails in view
            min_visible_thumb = int((self.GetViewStart()[1]*self.ScrollRate) / self.ThumbnailHeight)
            max_visible_thumb = min_visible_thumb + int(height / self.ThumbnailHeight) + 2
    
            # TEMP - Print min/max visible thumbnails
            print(f"min_visible_thumb = {min_visible_thumb}")
            print(f"max_visible_thumb = {max_visible_thumb}")
            print(f"height = {height}")
    
            for n in range(min_visible_thumb, max_visible_thumb):
                # Define the thumnail rectangle
                r = wx.Rect(0, n*100, width, 100)
                
                # Set the color alternate between red, green, or blue
                dc.SetBrush(wx.Brush(wx.Colour(255 * (n % 3 == 0), 255 * (n % 3 == 1), 255 * (n % 3 == 2))))
                
                # Draw the background
                dc.DrawRectangle(r)
    
                # Draw the text label
                dc.SetTextForeground(wx.WHITE)
                dc.DrawLabel(str(n), r, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)
    
    eng = wx.App()
    frame = wx.Frame(parent=None, title='wxPython')
    
    main_panel = wx.Panel(frame)
    main_sizer = wx.BoxSizer(wx.HORIZONTAL)
    main_panel.SetSizer(main_sizer)
    
    left_panel = ThumbnailView(main_panel)
    main_sizer.Add(left_panel, 1, wx.ALL | wx.EXPAND, 0)
    
    right_panel = wx.Panel(main_panel)
    main_sizer.Add(right_panel, 4, wx.ALL | wx.EXPAND, 0)
    
    frame.Show()
    frame.SetSize(1440,875)
    frame.CenterOnScreen()
    
    eng.MainLoop()
    

    Here's the screenshot of the prototype:

    wxPython prototype wx.Scrolled

    This approach scales very well to thousands, tens of thousands, or a hundred thousand rows while only needing to draw the displayed rows.