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)
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()
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:
This approach scales very well to thousands, tens of thousands, or a hundred thousand rows while only needing to draw the displayed rows.