Good day,

Hi, I'm trying to develop mapping feature for a tool. It has a wx.ScrolledWindow that contains wx.lib.buttons.GenBitmapTextButton as map nodes. When the number of map nodes reached around 50+ the scrolling becomes too slow. Any ideas on how to improve the performance of the scrolling when there are lots of items inside the wx.ScrolledWindow?

Thanks much!

Dani AI

Generated

Good catch by — the slowdown was caused by per-node tooltips. That matches ’s hint that how the bitmap-button objects (and their ancillary work) are created matters: mass-updating tooltip text for many child windows during layout/scroll forces a lot of native work and event churn, which kills scroll performance.

Practical, low-effort fixes

  • Stop updating every node’s tooltip repeatedly. If the tooltip text is static, set it once when each node is created and never recompute it.
  • If tooltips must be dynamic, compute and attach them on demand only for the node actually being hovered, and debounce the display so rapid mouse movement or scrolling does not trigger many updates.

Lightweight pattern to show tooltips on hover (debounced)

  • create a single wx.Timer in the parent
  • bind EVT_ENTER_WINDOW/EVT_LEAVE_WINDOW for each node to track the hovered node
  • start a short one-shot timer (e.g. 200–300 ms) on enter and clear it on leave; when the timer fires, compute and set the tooltip for that single node

Example (illustrative):

# parent initialization
self._ttTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self._onTooltipTimer, self._ttTimer)
self._hoverNode = None

# per-node
node.Bind(wx.EVT_ENTER_WINDOW, self._onNodeEnter)
node.Bind(wx.EVT_LEAVE_WINDOW, self._onNodeLeave)

def _onNodeEnter(self, evt):
    self._hoverNode = evt.GetEventObject()
    self._ttTimer.Start(250, True)  # one-shot

def _onNodeLeave(self, evt):
    self._ttTimer.Stop()
    if self._hoverNode:
        self._hoverNode.SetToolTip("")

def _onTooltipTimer(self, evt):
    if not self._hoverNode:
        return
    tip = make_tooltip_for(self._hoverNode)  # compute only once
    self._hoverNode.SetToolTip(tip)

More scalable approach

  • For many (50+) items prefer owner-drawn rendering on a single scrolled canvas: draw bitmaps and labels yourself, handle hit-testing and hover there. That eliminates hundreds of child windows and scales far better.
  • Also use Freeze/Thaw or hide the parent while populating, and use buffered painting to reduce flicker. Profile with cProfile to find other hotspots.

These steps preserve the helpful hover information while avoiding the heavy cost of mass tooltip updates.

Recommended Answers

All 3 Replies

A lot depends on how you create your bitmap-button objects.

i have this sub class of GenBitmapTextButton

import wx.lib.buttons as wxButtons
import images
import core
import wx
class mapNode(wxButtons.GenBitmapTextButton):
    def __init__(self, parent, pyData):
        label, name, bitmap, rspFlag, obFlag = self.getProperties(pyData)
        wxButtons.GenBitmapTextButton.__init__(self, parent, id=-1, bitmap=bitmap, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=name, label=label)
        self.SetFont(wx.Font(pointSize=7, style=90, family=74, weight=90))
        self.pyData = pyData
        self.rspFlag = rspFlag
        self.obFlag = obFlag
    def setPyData(self, pyData):
        self.pyData = pyData
    def DoGetBestSize(self):
        return (80, 30)
    
    def getProperties(self, pyData):
        rspFlag=0
        obFlag=0
        print str(pyData)
        if type(pyData) == core.site:
            label = pyData.database
            name = pyData.database
            bitmap = images.getSiteBitmap()
        elif type(pyData) == core.workflow:
            if pyData.depNo == '-1':
                lblPrefix = '[' +  pyData.creditorNo + '] '
            else:
                lblPrefix = '[' +  pyData.creditorNo + ':' + pyData.depNo + '] '
            label = lblPrefix + pyData.creditorName 
            name = lblPrefix + pyData.creditorName 
            bitmap = images.getWorkflowBitmap() 
        elif type(pyData) == core.category:
            label = pyData.categoryName
            name = pyData.categoryName
            bitmap = images.getFolderCloseBitmap()
        elif type(pyData) == core.tree:
            label = pyData.treeName
            name = pyData.treeName
            bitmap = images.getTreeBitmap()
            if pyData.dctResponseContainer:
                rspFlag = 1
        elif type(pyData) in core.lstActionTypes:
            label = pyData.actionCode
            name = pyData.actionCode
            bitmap = self.getBitmap(pyData)
            if pyData.obCode:
                obFlag=1
            if pyData.dctResponseContainer:
                rspFlag = 1
        elif type(pyData) == core.response:
            label = pyData.responseName
            name = pyData.responseName
            bitmap = images.getResponseBitmap()
            if pyData.obCode:
                obFlag = 1
        return label, name, bitmap, rspFlag, obFlag
    
    def getBitmap(self, acn):
        dctBitmaps = {core.LetterAction: images.getLetterBitmap(), core.PhoneAction: images.getPhoneBitmap(),     core.ManualAction: images.getManualBitmap(),
                    core.GoToAction: images.getGotoBitmap(),       core.JumpToAction: images.getJumptoBitmap(),   core.ReturnToAction: images.getReturntoBitmap(),
                    core.WaitAction: images.getWaitBitmap(),       core.CloseAction: images.getCloseBitmap(),     core.AmendAction: images.getAmendBitmap(), 
                    core.ChangeAction: images.getChangeBitmap(),   core.CostsAction: images.getCostsBitmap(),     core.ErrorAction: images.getErrorBitmap(),
                    core.ExportAction: images.getExportBitmap(),   core.ListAction: images.getListBitmap(),       core.MoveAction: images.getMoveBitmap(),
                    core.NoteAction: images.getNoteBitmap(),       core.UpdateAction: images.getUpdatingBitmap(), core.UnknownAction: images.getFolderBitmap()}
                    
        if dctBitmaps.has_key(type(acn)):
            return dctBitmaps.get(type(acn))
        else:
            return images.getFolderBitmap()

then on other classes i use the method below to populate the map matrix and the map panel (w/c is an instance of wx.ScrolledWindow) with mapNodes

def populateMapMatrix(self, ste):
        self.dctMapMatrix[(0,0)] = mapNode(self.pnlMap, ste)
        self.dctMergedCategories = self.getMergedCategories(ste)
        y = 35
        for wfk, wf in ste.dctWorkflowContainer.items():
            self.dctMapMatrix[(0,y)] = mapNode(self.pnlMap, wf)
            y += 35
        x = 85
        
        for catk, cat in self.dctMergedCategories.items(): 
            self.dctMapMatrix[(x,35)] = mapNode(self.pnlMap, cat)
            for trek, tre in cat.dctTreeContainer.items(): 
                self.dctMapMatrix[(x,70)] = mapNode(self.pnlMap, tre)
                y = 105
                for acn in tre.getSortedActions():
                    self.dctMapMatrix[(x,y)] = mapNode(self.pnlMap, acn)
                    y += 35
                x += 85
    def populatePanelMap(self):
        for pos, node in self.dctMapMatrix.items():
            if node:
                node.SetPosition(pos)
                self.bindNodeEvents(node)
        self.updateNodeToolTipString(None)

sorry for the not so presentable code

found out that ToolTips attached to the nodes are causing the slowdown. removed those and scrolling process is now ok.

def updateNodeToolTipString(self, evt):
        for mxk, mxNode in self.dctMapMatrix.items():
            if mxNode:
                mxNode.SetToolTipString("mx:" + str(mxk) + " mp:" + str(self.pnlMap.CalcUnscrolledPosition(mxNode.GetPositionTuple())))
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.