Hiding and showing widgets with Show()/Hide()

I'm probably missing something very obvious here, but what is the correct procedure for (dynamically) hiding and showing widgets (panels, etc.) in wxWidgets/wxPython?

The documentation seems to suggest that calling Show()/Hide(), followed by Layout(), would be enough to make the change visible. However, in the following little sample app, things do not seem to work quite that way:

--- 8< ---

#!/usr/bin/env python
# -*- encoding: utf-8

import wx
import random

class ButtonPanel(wx.Panel):

    def __init__(self, *args, **kwargs):

        color = kwargs.pop('color')
        wx.Panel.__init__(self, *args, **kwargs)
        self.SetBackgroundColour(color)

        button = wx.Button(self, label="Toggle yellow panel")
        colorpanel = ColorPanel(
            self, style=wx.SUNKEN_BORDER,
            color=wx.Colour(240,240,0)
        )

        sizer = wx.BoxSizer(orient=wx.VERTICAL)
        sizer.Add(button, 0, wx.ALL|wx.ALIGN_RIGHT, 8)
        sizer.Add(colorpanel, 0, wx.EXPAND|wx.ALL^wx.TOP, 8)

        # Hide yellow colorpanels initially!
        colorpanel.Hide()

        self.SetSizer(sizer)

        self.Bind(wx.EVT_BUTTON, self.OnButton, button)
        self.colorpanel = colorpanel

    def OnButton(self, evt):
        """ Toggle the visibility of the yellow pane """

        if self.colorpanel.IsShown():
            self.colorpanel.Hide()
        else:
            self.colorpanel.Show()

        self.GetSizer().Layout()

class ColorPanel(wx.Panel):

    def __init__(self, *args, **kwargs):

        color = kwargs.pop('color')
        wx.Panel.__init__(self, *args, **kwargs)
        self.SetBackgroundColour(color)
        self.SetMinSize((10,50))

class ContainerPanel(wx.Panel):

    def __init__(self, *args, **kwargs):

        wx.Panel.__init__(self, *args, **kwargs)

        container = wx.ScrolledWindow(
            self,
            style=wx.SUNKEN_BORDER
        )
        container.SetScrollRate(8, 8)
        self.container = container

        contentsizer = wx.BoxSizer(orient=wx.VERTICAL)
        container.SetSizer(contentsizer)
        self.contentsizer = contentsizer

        color1 = wx.Colour(100, 200, 100)
        color2 = wx.Colour(190, 240, 190)

        for i in xrange(10):
            self.AddPanel(color1)
            self.AddPanel(color2)

        bordersizer = wx.BoxSizer()
        bordersizer.Add(container, 1, wx.EXPAND|wx.ALL, 2)
        self.SetSizer(bordersizer)

    def AddPanel(self, color):
        panel = ButtonPanel(self.container, color=color)
        self.contentsizer.Add(panel, 0, wx.EXPAND)

class TestFrame(wx.Frame):

    def __init__(self):

        wx.Frame.__init__(
            self,
            parent=None,
            title=u"Dynamic Panel Show/Hide Test",
            size=(300,300)
        )

        panel = ContainerPanel(self)

        sizer = wx.BoxSizer()
        sizer.Add(panel, 1, wx.EXPAND)
        self.SetSizer(sizer)

class TestApp(wx.App):

    def __init__(self):

        wx.App.__init__(self, redirect=False)
        frame = TestFrame()
        frame.Show(True)

if __name__ == '__main__':
    app = TestApp()
    app.MainLoop()

--- 8< ---

Each colored panel on the list has a button that should toggle the visibility of a yellow (sunken) subpanel. This toggling happens in the OnButton() button event handler.

But while the Show()/Hide() calls will toggle the state of the panel in the internal bookkeeping of wxPython, this change will only have visible effect on the screen after the user has resized the frame or otherwise caused a repaint event to occur, at least on Windows.

So... what am I doing wrong here?

(I've also tried calling Refresh() etc., but to no avail.)

···

--
znark

Jukka Aho wrote :

Each colored panel on the list has a button that should toggle the
visibility of a yellow (sunken) subpanel. This toggling happens in
the OnButton() button event handler.

But while the Show()/Hide() calls will toggle the state of the panel
in the internal bookkeeping of wxPython, this change will only have
visible effect on the screen after the user has resized the frame or
otherwise caused a repaint event to occur, at least on Windows.

I simply added a call to the ScrolledWindow's Layout() method and it
works for me on Linux (wxPython 2.6 and 2.8).

def OnButton(self, evt):
        """ Toggle the visibility of the yellow pane """
        ...
        self.GetSizer().Layout()
        self.GetParent().Layout() # container layout

wxtest.py (2.74 KB)

···

--
Benjamin Longuet

I was just about to write the same thing,

You should call container window's Layout(), not the sizer's

···

-----Original Message-----
From: wxpython-users-bounces@lists.wxwidgets.org
[mailto:wxpython-users-bounces@lists.wxwidgets.org] On Behalf Of Benjamin
Longuet
Sent: Thursday, July 10, 2008 1:24 PM
To: wxpython-users@lists.wxwidgets.org
Subject: Re: [wxpython-users] Hiding and showing widgets with Show()/Hide()

Jukka Aho wrote :

Each colored panel on the list has a button that should toggle the
visibility of a yellow (sunken) subpanel. This toggling happens in the
OnButton() button event handler.

But while the Show()/Hide() calls will toggle the state of the panel
in the internal bookkeeping of wxPython, this change will only have
visible effect on the screen after the user has resized the frame or
otherwise caused a repaint event to occur, at least on Windows.

I simply added a call to the ScrolledWindow's Layout() method and it works
for me on Linux (wxPython 2.6 and 2.8).

def OnButton(self, evt):
        """ Toggle the visibility of the yellow pane """
        ...
        self.GetSizer().Layout()
        self.GetParent().Layout() # container layout

--
Benjamin Longuet

Jukka Aho wrote :

>> But while the Show()/Hide() calls will toggle the state of the
>> panel in the internal bookkeeping of wxPython, this change will
>> only have visible effect on the screen after the user has resized
>> the frame or otherwise caused a repaint event to occur, at least
>> on Windows.
>
> I simply added a call to the ScrolledWindow's Layout() method and
> it works for me on Linux (wxPython 2.6 and 2.8).
>
> def OnButton(self, evt):
> """ Toggle the visibility of the yellow pane """
> ...
> self.GetSizer().Layout()
> self.GetParent().Layout() # container layout

Thanks, that seems to help a bit - the yellow panels now appear and
disappear immediately when you click on the "Toggle" buttons.

However, there's still something rather strange going on...

If you click the yellow panels open near the bottom of the list, you
will notice, at least on MSW, that wx.ScrolledWindow's scrollbar
never updates its size to accommodate the new size of the items in
the sizer, and the bottommost items on the list get pushed out of
view:

Exactly. It has the same behavior on Linux.

Try to do the layout of the frame, that should be better :

def OnButton(self, evt):
        """ Toggle the visibility of the yellow pane """

        if self.colorpanel.IsShown():
            self.colorpanel.Hide()
        else:
            self.colorpanel.Show()

        #self.GetSizer().Layout()
        self.GetTopLevelParent().Layout() # frame layout

···

--
Benjamin

Jukka Aho wrote:

Vladiuz wrote:

I was just about to write the same thing,

You should call container window's Layout(), not the sizer's

Thanks for the suggestion, but while that helps a bit, it does not seem to fully fix the problem. (Please see my second post to this thread for details and screenshots.)

Curiously, wxPython documentation seems to suggest otherwise: that calling the (immediate) sizer's Hide() (or Show()) function [1], then the sizer's Layout(), would be sufficient to hide and show individual items in that sizer:

<wxPython API Documentation — wxPython Phoenix 4.2.2 documentation;
<wxPython API Documentation — wxPython Phoenix 4.2.2 documentation;

It does not mention anything about also having to call the container (parent) _window's_ Layout() to complete the effect... and as I explained in the other post, even if I do that, ScrolledWindow's virtual size is still being handled incorrectly and there will be occasional rendering glitches until the user resizes the window.

That is because you don't have to. You only have to call Layout on the highest level up widget which you want to resize. What you are seeing as a "bug" or "undesired behavior" is exactly what someone else wants. If you hide something in Panel C, then you probably want to Layout Panel C, as the docs suggest. That MAY cause Panel B which contains to Panel C to look undesired, depending on your context, and as such you will need to explicitly tell Panel B to Layout to "complete" your effect, which you personally have defined for your application. In some applications I think you will find the effect is "completed" with a single layout call and in fact laying out the parent causes undesired behavior. I have run into this issue before, and the solution is being explicit and telling the highest level up sizer which may be affected to resize, since sizers cannot magically know when you want them to Layout.

I think you just need to think about your problem differently and realize in this case it really is a feature, not a bug, and that the only way for sizers to know how you want them to behave is to tell them.

Hopefully this helps,
Mike

Benjamin Longuet wrote:

self.GetTopLevelParent().Layout() # frame layout

Does it work for you on some other platform?

Yes, it works well on my Linux desktop. The yellow panel gets
the correct size, despite little flickering of a small square
sometimes, and the virtual size of the wx.ScrolledWindow is
instantaneously refreshed. No need to manually resize the
window.

Strange. But I think I may have now found a solution:

--- 8< ---

    def OnButton(self, evt):
        """ Toggle the visibility of the yellow pane """

        if self.colorpanel.IsShown():
            self.colorpanel.Hide()
        else:
            self.colorpanel.Show()

        # The "Parent" here is a wx.ScrolledWindow:
        self.Parent.FitInside()

--- 8< ---

This seems to work for me, and now both the visibility of the yellow panels and the positioning and size of the scrollbar (in other words: the virtual size of the parent wx.ScrolledWindow) update immediately, as they should.

Unfortunately, even with those two issues fixed, I still occasionally get these rendering glitches when I keep clicking the "Toggle" buttons randomly and resizing the window for a while:

<http://ronsu.homeip.net/showhide05.png&gt;

I'm not sure what what to do about them, and what might cause them in the first place, so I'm still suspecting I'm probably doing something in a way that goes against the "official" method of implementing these show-and-hide folding panel toggle effects... whatever that secret methodology might be. :frowning:

···

--
znark

Jukka Aho wrote:

       # The "Parent" here is a wx.ScrolledWindow:
       self.Parent.FitInside()

[...]

Unfortunately, even with those two issues fixed, I still occasionally
get these rendering glitches

OK, I found a fix for that one, too:

--- 8< ---

        # The "Parent" here is a wx.ScrolledWindow:
        self.Parent.FitInside()
        self.Parent.Refresh()

--- 8< ---

No more rendering glitches, and I guess I'm a happy bugger now... although I'm still not too sure if this "solution" is some awful ad-hockery or just The Way It Needs To Be Done(TM).

Thanks to all the participants, anyway. Now move along, there's nothing to see here!

···

--
znark