Fundamental GUI layout issue

I have this quite minimal example which gives unexpected layout. I fail to understand why control1 and control2 is layed out differently. control1 looks okay but in control2 the two text labels are on top of each other. Can anyone see what I’m doing wrong?

import wx

RH = 25  # control height
LW = 100 # control width

class MyCustomWidget(wx.Panel):
    def __init__(self, parent, value=0, labeltext='', size=(540, 25)):
        wx.Panel.__init__(self, parent, wx.ID_ANY, size=size, style=wx.WANTS_CHARS)

        self.parmSizer  = wx.BoxSizer(wx.HORIZONTAL)
        self.label1     = wx.StaticText(self, wx.ID_ANY, labeltext)
        self.label2     = wx.StaticText(self, wx.ID_ANY, 'Blah blah')

        self.parmSizer.Add(self.label1, 0, wx.ALL, 0)
        self.parmSizer.Add(self.label2, 0, wx.ALL, 0)

        # layout the widgets
        self.SetSizer(self.parmSizer)


class Settings(wx.Frame):
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 'Example', size=(600, 300))


        self.p  = wx.Panel(self, wx.ID_ANY, size = (LW, RH))

        # create some sizers
        mainSizer = wx.BoxSizer(wx.VERTICAL)

        self.control1 = MyCustomWidget(self.p, 0, 'This is control1')
        self.control2 = MyCustomWidget(self.p, 0, 'This is control2')

        # layout the widgets
        mainSizer.Add(self.control1)
        mainSizer.Add(self.control2)
        self.p.SetSizer(mainSizer)


def main():

    app = wx.App()
    ex = Settings()
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

The code works for me as expected. OpenSuse 15.1, wxPython 4.0.7, python 3.6.x.
What platform are you using, versions of wx and python would help us test.

Since you are using fixed sizes for the frame and panels then there is no initial size event after the frame is shown and the main loop is started. It’s in the default size event handler where the sizer-based layout is handled.

One way to fix your issue is to trigger the initial sizer layout yourself. For example, by adding this line to the end of your Settings.__init__:

    wx.CallAfter(self.p.Layout)

Hi Robin, Thank you. Indeed the controls are now in the expected places. But I’m not sure I understand your explanation. If I understand your explanation correctly, then when using absolute position/size then one has to explicitly call layout()? whereas if a sizer is used then this is not needed…

Also why not just call self.p.Layout() directly? what is the catch (sorry I could maybe google this one first)

Hi Robin,
Can you explain why the code worked on my OpenSuse 15.1, wxPython 4.0.7, python 3.6.x?
Johnf

The automatic layout functionality happens in the default EVT_SIZE handlers. There’s more to it than this, but you can think of the default size event handler like this:

    def OnSize(self, event):
        if self.GetSizer():
            self.Layout()

Usually there is an initial size event as the top-level windows are shown, or when the MainLoop starts up. But in some cases, usually when the top-level window is given a fixed size in its constructor, and nothing else happens that would change the frame’s client area, then the initial size event may not happen, because there wasn’t a size change that would trigger it. So in those cases you need to help out a little and tell it to run the layout algorithm as things are starting up.

The flipside of that thought is that if you avoid using fixed sizes when you don’t need them you you will probably avoid this issue. For example, your MyCustomWidget class has a sizer, and the widget will be used in a sizer, so there is no need to give it a fixed size when calling the parent’s __init__. In your frame class, if you delay setting the size until the end of your __init__ method after all the widgets have been created, like calling self.SetSize((600,300)), then the problem will probably be avoided as well. And if your frame has its own sizer (containing just self.p in this case, set to expand and fill) then you can just call self.Fit() at the end of the frame’s __init__ instead of the SetSize and it should size the frame to fit its contents, as calculated by the sizers in the window hierarchy.

Doing it via wx.CallAfter is just so it will call Layout after the MainLoop starts running. It would likely work okay to call it directly, but I like to defer it in case there are some other adjustments that might happen as other things happen in the application.

Because wxGTK does not typically have the problem of sometimes not having an initial size event. That’s because the Window Manager is a separate process and as the display server shows the window and and the Window Manager wraps the window decorations around it (the caption bar, borders, etc.) it will usually tweak the size of the window (your application’s part of the wx.Frame for example) and so there will be an immediate size event once events start flowing. The only time I’ve seen the issue happen on wxGTK is when the window is created with no caption, borders, etc. But that may vary across different desktop environments.

Thanks - it makes sense. Of course the poster did provide platform info and that made a difference. I’ll remember that fact.

For what is worth, I can’t see the problem on Windows either.
Anyways, thanks Robin for taking the time to delve deeper into this. I would advise against using fixed sizes in general - however, I find that a well-timed SendSizeEvent also works wonders in such cases.

This is my platform info

Microsoft Windows [Version 10.0.19041.450]
(c) 2020 Microsoft Corporation. All rights reserved.

H:\>python
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import wx
>>> wx.version()
'4.0.7.post2 msw (phoenix) wxWidgets 3.0.5'

Thanks for taking the time to explain this behavior.