Search code examples
pythonscrollwxpythoneventhandler

How do I make my wxPython scrolledPanel scroll to the bottom of the window?


Problem: I want to be able to force the scrollbar to the bottom when I call createNewRow(). I can see that the self.Scroll(0, self.scrollRange) is occurring in OnKeyTyped() and the scrollbar moves to the bottom of the window but then the scrollbar moves to the top of the window again. I tried to stop this by binding wx.EVT_SCROLLWIN to OnScroll which calls event.Skip() but it appears that this has not worked. I am out of ideas on how to proceed as I don't know enough about eventHandlers and scroll events in wxPython. Any help on how to proceed would be much appreciated. Full code below.

import os
import wx
import datetime as dt
import wx.lib.scrolledpanel as scrolled


class MyFrame(wx.Frame):
    width = 1000
    height = 600
    today = dt.date.today()
    today_date = f"{today:%A - %d %B %Y}"
    filename = f"Worklog {today_date}"
    wxTHICK_LINE_BORDER = 3

    def __init__(self, parent=None, title=filename, size=(width,height - 1)):
        wx.Frame.__init__(self, parent=parent, title=title, size=size)
        self.parent = parent
        self.title = title
        self.size = size
        self.BuildMenuBar()

    def BuildMenuBar(self):
        # Menu bar
        self.menuBar = wx.MenuBar()
        self.fileMenu = wx.Menu()
        self.NewOpt = wx.MenuItem(self.fileMenu, wx.ID_NEW, '&New\tCtrl+N')
        self.OpenOpt = wx.MenuItem(self.fileMenu, wx.ID_OPEN, '&Open\tCtrl+O')
        self.SaveOpt = wx.MenuItem(self.fileMenu, wx.ID_SAVE, '&Save\tCtrl+S')
        self.QuitOpt = wx.MenuItem(self.fileMenu, wx.ID_EXIT, '&Quit\tCtrl+Q')
        self.fileMenu.Append(self.NewOpt)
        self.fileMenu.Append(self.OpenOpt)
        self.fileMenu.Append(self.SaveOpt)
        self.fileMenu.Append(self.QuitOpt)
        self.Bind(wx.EVT_MENU, self.OnQuit, self.QuitOpt)
        self.menuBar.Append(self.fileMenu, '&File')
        self.SetMenuBar(self.menuBar)


    def OnQuit(self, e):
        self.Close()


class MyPanel(wx.Panel):
    def __init__(self,parent):
        wx.Panel.__init__(self, parent=parent)
        self.parent = parent
        self.size = parent.size
        panel_colour = wx.Colour(240, 240, 240, 255)
        self.SetBackgroundColour(panel_colour)
        self.Refresh()


class MyScrolledPanel(scrolled.ScrolledPanel):
    def __init__(self, parent):
        scrolled.ScrolledPanel.__init__(self, parent=parent, style = wx.TAB_TRAVERSAL | wx.TB_BOTTOM)
        self.parent = parent
        # self.size = parent.size
        self.width = parent.size[0]
        self.height = parent.size[1]
        scrollpanel_colour = wx.Colour(255, 255, 255, 255)
        self.SetBackgroundColour(scrollpanel_colour)
        # Call a refresh to update the UI
        self.Refresh()
        self.SetAutoLayout(True)
        self.SetupScrolling()
        self.InitUI()
        self.Bind(wx.EVT_SCROLLWIN, self.OnScroll, self)
        self.Bind(wx.EVT_SIZE, self.OnSize, self)


    def OnScroll(self, e):
        e.Skip()


    def InitUI(self):
        vgap = 0
        hgap = 0

        self.rowList = []

        self.n = 0

        self.scrollSizer = wx.GridBagSizer(vgap + 10, hgap + 10)    

        self.row = self.CreateNewRow(self.n)

        self.rowList.append(self.row)
        print(f"Row List: {self.rowList[-1]}")

        self.scrollSizer.Add(self.row[0], pos = (self.i, 0), flag = wx.ALL | wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL, border = 10)
        self.scrollSizer.Add(self.row[1], pos = (self.i, 1), flag = wx.EXPAND | wx.TOP | wx.RIGHT | wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL , border = 10)

        self.scrollSizer.AddGrowableCol(1)
        self.SetSizer(self.scrollSizer)

        self.panelSizer = wx.GridBagSizer(vgap, hgap)
        self.panelSizer.AddGrowableRow(0)
        self.panelSizer.AddGrowableCol(0)
        self.panelSizer.Add(self, pos = (0, 0), flag = wx.EXPAND, border = 0) # Add wx.Window not wx.Sizer
        self.parent.SetSizer(self.panelSizer)


    def CreateNewRow(self, number):
        self.i = number
        self.txtStr = "%02d" % (self.i+1) + ". "
        self.staticText = wx.StaticText(self, wx.ID_ANY, self.txtStr)
                        #pos = (x, y)
        #self.staticText.SetForegroundColour(wx.Colour(0,0,0))

        self.control = wx.TextCtrl(self, self.i)
        self.control.SetMaxLength(256)
        self.text_history_length = 0
        self.control.Bind(wx.EVT_TEXT, self.OnKeyTyped, id = self.i)
        #self.control = wx.TextCtrl(self, -1, pos = (x + w + 5,y) )
                        #style = wx.TE_MULTILINE
        elems = [self.staticText, self.control]

        return elems


    def OnSize(self, e):
        self.width, self.height = e.GetSize()
        self.SetSize((self.width, self.height))
        self.OnSizeChange()
        self.Refresh()


    def OnSizeChange(self):
        # Fit child elements
        self.scrollSizer.FitInside(self)
        # Resize layout
        self.Layout()
        # Resize scrolling
        self.SetupScrolling()


    def OnKeyTyped(self, e):
        self.text_length = len(e.GetString())
        if (self.text_history_length == 1 and self.text_length == 0):
            print(f"History length: {self.text_history_length}")
            print(f"Text length: {self.text_length}")
            self.text_history_length = self.text_length
            pass
        elif (self.text_history_length == 0 and self.text_length == 1):
            print(f"History length: {self.text_history_length}")
            print(f"Text length: {self.text_length}")
            self.n += 1
            self.row = self.CreateNewRow(self.n)
            print(f"Action: {self.row}")
            print(f"Row List: {self.rowList[-1]}")
            self.rowList.append(self.row)
            self.scrollSizer.Add(self.row[0], pos = (self.n, 0), flag = wx.ALL | wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL, border = 10)
            self.scrollSizer.Add(self.row[1], pos = (self.n, 1), flag = wx.EXPAND | wx.TOP | wx.RIGHT | wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL , border = 10)
            self.SetupScrolling()
            self.text_history_length = self.text_length
            self.rowList[self.n-1][1].Bind(wx.EVT_TEXT, None, id = self.n-1)
            self.text_history_length = 0
        else:
            print(f"History length: {self.text_history_length}")
            print(f"Text length: {self.text_length}")
            self.text_history_length = self.text_length
        self.rowList[-1][1].SetFocus()
        self.scrolledPanelChild = self.GetChildren()[-1] # [ scrollPanel ]
        self.ScrollChildIntoView(self.scrolledPanelChild)
        self.OnSizeChange()
        self.scrollRange = self.GetScrollRange(wx.VERTICAL)
        print(f"ScrollRange: {self.scrollRange}")
        #self.scrollUnits = self.GetScrollPixelsPerUnit()
        #print(f"ScrollUnit: {self.scrollUnits}")
        #self.scrollThumb = self.GetScrollThumb(wx.VERTICAL)
        #print(f"ScrollThumb: {self.scrollThumb}")
        self.Scroll(0, self.scrollRange)


def main():
    app = wx.App(False)
    app.locale = wx.Locale(wx.Locale.GetSystemLanguage())
    frame = MyFrame()
    panel = MyPanel(frame)
    scrolledPanel = MyScrolledPanel(panel)
    frame.Show(True)
    app.MainLoop()


if __name__ == "__main__":
    main()

Solution

  • Old question but still. This can be solved using: wx.CallAfter(self._scrolled_panel.ScrollChildIntoView, new_text) Or calling any of the other scroll methods from the CallAfter.
    or
    SetupScrolling(scrollIntoView=True, scrollToTop=False)