Making a dynamic scrolledPanel

Hi, I’m trying to build a dynamic scrolledPanel for my application.

I have a wx.CheckBox and a wx.StaticText, wrapped in a horizontal sizer, that I want to put in a list called here sizerRefs. In my app, depending on user input, I want to display one or more of these sizers on the scrolledPanel. But, for some reason, my app is display all of them without my command. The result is the last sizer appering on top of everyone else.

Is there a way I can make this work?
Thanks!

Expected result:
expected

import wx
import wx.lib.scrolledpanel as scrolled

class WaterDataBase(wx.Frame):
    def __init__(self, parent):
        style = wx.DEFAULT_FRAME_STYLE & (~wx.MAXIMIZE_BOX)

        wx.Frame.__init__(self, parent, style=style)
        self.sizerRefs = []
        self.setupSizers()

        self.populateRefs()
        self.appendWordSizer(2)

    def setupSizers(self):
        self.masterSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.itemsSizer = wx.BoxSizer(wx.VERTICAL)

        self.scrolled_panel = scrolled.ScrolledPanel(self, -1, style=wx.SUNKEN_BORDER, size=(400, 400))
        self.scrolled_panel.SetupScrolling()
        self.scrolled_panel.SetSizer(self.itemsSizer)

        self.masterSizer.Add(self.itemsSizer)

    def initWordSizer(self, word):
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        checkBox = wx.CheckBox(self, -1)
        text = wx.StaticText(self, -1, word)

        sizer.Add(checkBox)
        sizer.Add(text)
        self.sizerRefs.append(sizer)

    def populateRefs(self):
        words = "I'm trying to make this work, please.".split()
        for word in words:
            self.initWordSizer(word)

    def appendWordSizer(self, index):
        self.itemsSizer.Add(self.sizerRefs[index])
        self.SetSizer(self.masterSizer)

app = wx.App()
frame = WaterDataBase(None).Show()
app.MainLoop()

I made it, but I’m not sure if it’s the mest method. Seems a lot like a word around to me.
Any suggestions? :smiley:
Thanks

Edit: My newest version deletes and creates the sizers from scratch instead of hiding and showing them. This proved the best decision for now. I’ll update the code below soon.

Edit2: The code is ready and updated. This is what I’m trying to achive and it’s pretty much ready. I’m still taking advices for improvements, especially for the allocation / deallocation for the sizers on the scrolledPanel. :smiley:

import wx
import wx.lib.scrolledpanel as scrolled

class WaterDataBase(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)

        self.setupSizers()
        self.setupData()

    def setupSizers(self):
        self.masterSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.itemsSizer = wx.BoxSizer(wx.VERTICAL)

        self.itemsSizer.Add(self.addSearchControl(), flag=wx.ALL, border=7)

        self.scrolledPanelSizer = wx.BoxSizer(wx.VERTICAL)
        self.scrolled_panel = scrolled.ScrolledPanel(self, wx.ID_ANY, size=(200, 200))
        self.scrolled_panel.SetSizer(self.scrolledPanelSizer)

        self.itemsSizer.Add(self.scrolled_panel, flag=wx.ALL, border=7)
        self.masterSizer.Add(self.itemsSizer)
        self.scrolled_panel.SetupScrolling()

        self.SetSizer(self.masterSizer)

    def addSearchControl(self):
        sizer = wx.BoxSizer(wx.HORIZONTAL)

        self.searchField = wx.TextCtrl(self, -1)
        self.searchField.Bind(wx.EVT_TEXT, self.OnSearched)
        sizer.Add(self.searchField)

        return sizer

    def setupData(self):
        self.words = "I'm trying to make this work, please. Let's keep it on! The day is beautiful today. Together we are stronger!".split()
        for word in self.words:
            self.addSizerToPanel(word)

    def createSizer(self, word):
        # Creates a sizer with a CheckBox and a StaticText to display.

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        checkBox = wx.CheckBox(self.scrolled_panel, -1)
        text = wx.StaticText(self.scrolled_panel, -1, word)

        sizer.Add(checkBox, flag=wx.ALL, border=5)
        sizer.Add(text, flag=wx.LEFT, border=5)

        return sizer

    def addSizerToPanel(self, word):
        sizer = self.createSizer(word)
        self.scrolledPanelSizer.Add(sizer, flag=wx.ALL, border=5)

    def OnSearched(self, event):
        query = self.searchField.GetValue().lower()
        result = []

        # If query's empty, print all words
        if not query or query.isspace():
            for word in self.words:
                result.append(word)
        else:
            for word in self.words:
                if word.lower().find(query) != -1:
                    result.append(word)

        # Destroy all panel sizers and put exactly the ones we want.
        self.scrolled_panel.DestroyChildren()
        for word in result:
            self.addSizerToPanel(word)

        self.scrolled_panel.Layout()
        self.scrolled_panel.Scroll(0, 0) # Using this to cause the scrollPanel get back to the top.

app = wx.App()
frame = WaterDataBase(None).Show()
app.MainLoop()

I’m not sure what you are actually after (incremental search ??) but if searching for say ‘y’, then maximising your ‘WaterDaterBase’ and all dynamic scrolling is drowned !! (too much water in the base ?) :roll_eyes:

Hi, what I’m trying to make is a more efficient code. The way things are handled now, make the scrolledPanel blinks while I’m adding / removing sizers and it seems to use quite bit of resources. Also, in my real app, I have lists of references for the buttons and checkBoxes and I’m having to deal with a lot of errors because I’m trying to acess references that have been deleted.

But I just made it work now (hiding / showing). So, we can say the problem is solved. But if anyone knows how to reset the size of the scrolledPanel when there are only a few sizers on the panel, it would be nice. :slight_smile:

If anyone wants to suggest any changes, I appreciate it. :smiley:

import wx
import wx.lib.scrolledpanel as scrolled

class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)

        self.sizerRefs = []
        self.words = []

        self.setupSizers()
        self.setupData()

    def setupSizers(self):
        masterSizer = wx.BoxSizer(wx.HORIZONTAL)
        itemsSizer = wx.BoxSizer(wx.VERTICAL)

        itemsSizer.Add(self.addSearchControl(), flag=wx.ALL, border=7)

        self.scrolledPanelSizer = wx.BoxSizer(wx.VERTICAL)
        self.scrolled_panel = scrolled.ScrolledPanel(self, wx.ID_ANY, size=(200, 200))
        self.scrolled_panel.SetSizer(self.scrolledPanelSizer)

        itemsSizer.Add(self.scrolled_panel, flag=wx.ALL, border=7)
        masterSizer.Add(itemsSizer)
        self.scrolled_panel.SetupScrolling()

        self.SetSizer(masterSizer)

    def addSearchControl(self):
        sizer = wx.BoxSizer(wx.HORIZONTAL)

        self.searchField = wx.TextCtrl(self, -1)
        self.searchField.Bind(wx.EVT_TEXT, self.OnSearched)
        sizer.Add(self.searchField)

        return sizer

    def setupData(self):
        self.words = "I'm trying to make this work, please. Let's keep it on! The day is beautiful today. Together we are stronger!".split()

        for i in range(0, len(self.words)):
            self.addSizerToPanel(i)

    def createSizer(self, index):
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        checkBox = wx.CheckBox(self.scrolled_panel, -1)
        text = wx.StaticText(self.scrolled_panel, -1, self.words[index])

        sizer.Add(checkBox, flag=wx.ALL, border=5)
        sizer.Add(text, flag=wx.LEFT, border=5)

        self.sizerRefs.append(sizer)
        return sizer

    def addSizerToPanel(self, index):
        sizer = self.createSizer(index)
        self.scrolledPanelSizer.Add(sizer, flag=wx.ALL, border=5)

    def hideAllSizers(self):
        for sizer in self.sizerRefs:
            sizer.ShowItems(False)

    def unhideSizer(self, index):
        self.sizerRefs[index].ShowItems(True)

    def OnSearched(self, event):
        query = self.searchField.GetValue().lower()
        result = [] # Storing the indexes of the words found

        # If query's empty, print all words
        if not query or query.isspace():
            for i in range(0, len(self.words)):
                result.append(i)
        else:
            for i in range(0, len(self.words)):
                if self.words[i].lower().find(query) != -1:
                    result.append(i)

        # Hides all panel sizers and unhide exactly the ones we want.
        self.hideAllSizers()
        for i in range(0, len(result)):
            self.unhideSizer(result[i])

        self.scrolled_panel.Layout()
        self.scrolled_panel.Scroll(0, 0) # Using this to cause the scrollPanel get back to the top.

app = wx.App()
frame = Frame(None).Show()
app.MainLoop()

basically I like efficient code, but more basic is that it works!!! and as far as the fault I pointed out before is concerned there hasn’t changed anything & vanishing scrollbars on a scrollable is a strong indicator that something is really bad (by the way, efficient in the string operation is asking for in, not find and the list ‘sizerRefs’ is probably redundant for wx knows all that)
so I would say give it another try: first the working & afterwards tuning :pensive:

Hi,

I guess what you are looking for is self.SendSizeEvent() or self.PostSizeEvent().
These methods manually trigger wx.SIZE_EVENT.

1 Like

If you can change the GUI design, CheckListCtrl may be better and more efficient.
I tested the following code on Python 3.8 and wx 4.1.0 on Windows.

import re
import wx

class CheckList(wx.ListCtrl):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.EnableCheckBoxes() # wx4.1.0 or later,
                                # below 4.0.7 use CheckListCtrlMixin
        
        self.words = ("I'm trying to make this work, please. "
                      "Let's keep it on! The day is beautiful today. "
                      "Together we are stronger!".split())
        
        self.InsertColumn(0, "word", width=100)
        
        for i, word in enumerate(self.words):
            self.InsertItem(i, word)
    
    def OnSearched(self, event):
        self.DeleteAllItems()
        
        query = re.compile(event.String, re.I)
        for i,word in enumerate(filter(query.match, self.words)):
            self.InsertItem(i, word)

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.lchk = CheckList(self, style=wx.LC_REPORT|wx.LC_HRULES|wx.LC_NO_HEADER)
        
        self.searchField = wx.TextCtrl(self)
        self.searchField.Bind(wx.EVT_TEXT, self.lchk.OnSearched)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.searchField, 0, wx.EXPAND)
        sizer.Add(self.lchk, 1, wx.EXPAND)
        self.SetSizer(sizer)

app = wx.App()
frame = Frame(None)
frame.Show()
app.MainLoop()
1 Like

Hey, thanks very much. :smiley:
I tested the self.SendSizeEvent() and it worked!

I couldn’t test the CheckListCtrl because I’m using buttons instead of text in my scrolledPanel. Sorry for withholding this information. People say you need to make your code simple as possible when asking for help, but I guess I didn’t need to in this situation.

Checkboxes in a list are fossils :sneezing_face:

image

inc_search.py (4.5 KB)

1 Like

I’ll take a look into your code, thank you! :smiley:

I think one can always improve…

inc_search.py (4.3 KB)

1 Like

The gist of the matter is a context aware TextCtrl which the user expects these days (of course, in a real app context will be some SQL or something more intelligent, need for speed: just ask Google)

test_frame_ctx_tc.py (756 Bytes)
ctx_textctrl.py (7.5 KB)

(the selection was somewhat flawed :sweat_smile:)