RichTextCtrl - very slow scrolling

I was investigating using a RichTextCtrl in a dialog to display and edit the contents of a spellchecker’s personal word list. In my testing I used a file containing about 850 lines with one word per line. I found that scrolling the list on my linux PC was very slow and rather jerky. Further experiments with 1000 lines or more and it became nearly unusable. In contrast, scrolling the same list in a StyledTextCtrl was still quick and fluid.

When I made the lines of text longer, the behaviour of the RichTextCtrl became rather strange. When I released the scroll bar thumb after scrolling downwards, the lines would then start scrolling back upwards very slowly which made it totally unusable.

I tried the same tests on a more powerful linux PC and the response of the RichTextCtrl was only a little better, and the CPU usage was still running over 70% while it was trying to catch up with the scrolling.

I am using Python 3.8.10 + wxPython 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5 + Linux Mint 20.3 on both PCs.

Does the same thing happen on other platforms, or is this just a Linux/GTK issue?

Below is a test app that has a RichTextCtrl. Clicking on one of the number buttons puts the corresponding number of lines in the control. If check box is checked the lines inserted will be longer.

import wx
import wx.richtext as rt

class MyDialog(wx.Dialog):
    def __init__(self, *args, **kwds):
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
        wx.Dialog.__init__(self, *args, **kwds)
        self.SetSize((500, 600))
        self.SetTitle("Sluggish Scroll in RichTextCtrl")
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        self.rtc = rt.RichTextCtrl(self, wx.ID_ANY, style=rt.RE_MULTILINE)
        main_sizer.Add(self.rtc, 1, wx.ALL | wx.EXPAND, 8)
        lines_sizer = wx.BoxSizer(wx.HORIZONTAL)
        main_sizer.Add(lines_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, 8)
        self.button_250 = wx.Button(self, wx.ID_ANY, "250")
        self.button_250.SetMinSize((65, 30))
        lines_sizer.Add(self.button_250, 0, wx.RIGHT, 8)
        self.button_500 = wx.Button(self, wx.ID_ANY, "500")
        self.button_500.SetMinSize((65, 30))
        lines_sizer.Add(self.button_500, 0, wx.RIGHT, 8)
        self.button_1000 = wx.Button(self, wx.ID_ANY, "1000")
        self.button_1000.SetMinSize((65, 30))
        lines_sizer.Add(self.button_1000, 0, wx.RIGHT, 8)
        self.button_1500 = wx.Button(self, wx.ID_ANY, "1500")
        self.button_1500.SetMinSize((65, 30))
        lines_sizer.Add(self.button_1500, 0, wx.RIGHT, 8)
        self.long_lines_checkbox = wx.CheckBox(self, wx.ID_ANY, "Longer lines")
        lines_sizer.Add(self.long_lines_checkbox, 0, wx.ALIGN_CENTER_VERTICAL, 0)
        bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
        main_sizer.Add(bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM | wx.TOP, 8)
        self.close_button = wx.Button(self, wx.ID_ANY, "Close")
        bottom_sizer.Add(self.close_button, 0, 0, 0)
        self.SetSizer(main_sizer)
        self.Layout()

        self.Bind(wx.EVT_BUTTON, self.OnClose, self.close_button)
        self.Bind(wx.EVT_BUTTON, self.On250,   self.button_250)
        self.Bind(wx.EVT_BUTTON, self.On500,   self.button_500)
        self.Bind(wx.EVT_BUTTON, self.On1000,  self.button_1000)
        self.Bind(wx.EVT_BUTTON, self.On1500,  self.button_1500)

    def setLines(self, num_lines):
        if self.long_lines_checkbox.GetValue():
            line = "This is a longer, longer, longer line %d"
        else:
            line = "Line %d"
        lines = [line % i for i in range(num_lines)]
        text = '\n'.join(lines)
        self.rtc.SetValue(text)
        self.rtc.ShowPosition(self.rtc.GetLastPosition())

    def On250(self, _event):
        self.setLines(250)

    def On500(self, _event):
        self.setLines(500)

    def On1000(self, _event):
        self.setLines(1000)

    def On1500(self, _event):
        self.setLines(1500)

    def OnClose(self, _event):
        self.EndModal(wx.ID_CLOSE)


class MyApp(wx.App):
    def OnInit(self):
        self.dialog = MyDialog(None, wx.ID_ANY, "")
        self.SetTopWindow(self.dialog)
        self.dialog.ShowModal()
        self.dialog.Destroy()
        return True


if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

Hi, Richard

I’ve tested the code, and it works fine on Windows 10 PC
(tested with wx 4.0.7/Python 3.8.6 and wx 4.1.1/Python 3.9.9 – the response is very smooth, almost no time lag, not much CPU consumption).

It might be a font problem on Linux system… I’m not sure if this helps to find the bottleneck, but how about monitoring events?

class MyApp(wx.App):
    def OnInit(self):
        self.dialog = MyDialog(None, wx.ID_ANY, "")
        self.SetTopWindow(self.dialog)
        if 1:
            from wx.lib.eventwatcher import EventWatcher
            ew = EventWatcher(None)
            ew.watch(self.dialog.rtc)
            ew.Show()
        self.dialog.ShowModal()
        self.dialog.Destroy()
        return True

Hi Kazuya,

Thanks for your reply and information about the behaviour on Windows.

I tried your suggestion of using the EventWatcher. I noticed that when I had short lines in the RTC and dragged the scrollbar, it was showing 2 EVT_SIZE events after each EVT_SCROLLWIN_THUMBTRACK event.

When I then tried it with longer lines and it got into the state where it was slowly scrolling up (i.e. the line numbers were decreasing at about 2 lines per second), then there was a continuous sequence of EVT_SIZE events.

What that means, I have no idea!

I also had a look at the RichTextCtrl example in the wxPython Demo which has a number of different styles and two images. In its initial form it scrolls smoothly and quickly. However, if I do Ctrl-A, Ctrl-C, Ctrl-End and Ctrl-V to add a copy of the contents and then repeat that until there are 8 copies of the original contents, then it also shows the slow jerky scrolling behaviour.

This is what I saw on Windows 10 and I never got EVT_SIZE while scrolling or thumb-tracking.
rt-scroll-speed-crop-drop

I guess the problem might be int/float conversion issue for window size…
Can you try to launch script by $ py -Wd <script.py> to see a bunch of warnings?

I tried running the script using $ py -Wd <script.py> but it didn’t output anything in the terminal window.

I remembered having a problem a while ago that was caused by the default font on Linux Mint having fractional widths and wondered if that might also be causing this problem. Therefore I modified the script to set the RichTextCtrl’s basic style to use a proportional font that doesn’t use fractional widths. This made a difference but didn’t completely fix the problem.

If I run the script using the default font, insert 1500 longer lines, and drag the scroll thumb upwards, the thumb moves in steps but the text in the RTC is frozen until I release the mouse button, at which point the text in the RTC suddenly jumps to position where it should be. However, it then starts slowly scrolling line by line towards the first line. If I leave it alone it will carry on scrolling slowly until it finally gets there. This can take a long time during which the interface is not usable.

If I do the same thing using the non fractional width font, it behaves the same, except it stops at the point where the mouse button is released and doesn’t do the continuous slow scrolling. So, it is more usable than the previous case, but the lack of visual feedback while scrolling is still poor when compared to the StyledTextCtrl.

The EVT_SIZE events are generated in both cases.

This is just an update to say that the very slow scrolling problem with the RichTextCtrl has gone away when tested using Python 3.10.4 + wxPython 4.2.0 gtk3 (phoenix) wxWidgets 3.2.0 + Linux Mint 21.

Scrolling is much improved on what I was getting when using wxPython 4.1.1 and, even on my 10 year old PC, it easily keeps up with rapid movements of the scrollbar thumb.