Search code examples
pythonwxpython

How to layout wxPython panels that expand both directions


I'm building a UI in wxPython,[note 1] and I have three main body panels, two toolbars, and a status bar, with two BoxSizer elements (a vertical one containing all of the above, and a horizontal one containing the body panels). I cannot get the layout to work quite right, and I'm getting some behavior that I can't find in the documentation. I'm leaving out some details, but here is the relevant (working) part of the app:

import wx
import wx.adv

class MyApp(wx.App):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def OnInit(self):
        self.frame = AppFrame(parent=None, title="My Sweet App")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

class AppFrame(wx.Frame):
    def __init__(self, size=(1024, 768), *args, **kwargs):
        super().__init__(size=size, *args, **kwargs)

        # Menu (quit only)
        menubar = wx.MenuBar()
        file_menu = wx.Menu()

        quit_item = wx.MenuItem(
            file_menu, wx.ID_EXIT, '&Exit', 'Close the application')
        file_menu.Append(quit_item)
        self.Bind(wx.EVT_MENU, OnQuit, id=wx.ID_EXIT)
        menubar.Append(file_menu, '&File')
        self.SetMenuBar(menubar)

        # Outer box wrapper
        main_box = wx.BoxSizer(orient=wx.VERTICAL)
        self.SetSizer(main_box)
        main_box.SetMinSize(size)

        # Inner box with the three main view panels in it
        wrap_panels_box = wx.BoxSizer(orient=wx.HORIZONTAL)
        wrap_panels_box.SetMinSize(200, 200)
        panel1 = wx.Panel(self, -1)
        panel2 = wx.Panel(self, -1)
        panel3 = wx.Panel(self, -1)
        common_flags = wx.SizerFlags().Expand().Border(wx.ALL, 5)
        wrap_panels_box.AddMany([(panel1, common_flags),
                                 (panel2, common_flags),
                                 (panel3, common_flags)])

        # Two toolbars for different sets of commands
        toolbar1 = wx.ToolBar(parent=self)
        tool1 = toolbar1.CreateTool(
            toolId=wx.ID_NEW, label='',
            bmpNormal=wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR))
        toolbar1.AddTool(tool1)
        toolbar1.Realize()

        toolbar2 = wx.ToolBar(parent=self)
        tool2 = toolbar2.CreateTool(
            toolId=wx.ID_SAVE, label='',
            bmpNormal=wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR))
        toolbar2.AddTool(tool2)
        toolbar2.Realize()

        statusbar = wx.StatusBar(parent=self)

        # Add all layout elements
        bar_flags = common_flags.Proportion(0)

        main_box.Add(toolbar1, bar_flags)
        main_box.Add(wrap_panels_box, common_flags.Proportion(1))
        main_box.Add(toolbar2, bar_flags)
        main_box.Add(statusbar, bar_flags)

        self.Layout()

def OnQuit(event):
    exit(0)

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

All the sub-component generation methods (_GenerateMenu(), _GenerateToolStatusBars(), and _GenerateViewPanels()) work as expected and basically as desired, so I'm leaving them aside.

The various pieces are largely in the right places, but I have a couple quirks here.

1. Status bar expansion

The status bar returned by the _GenerateToolStatusBars() method acts like it has Proportion(1) set on it: it expands or contracts vertically as the main window is expanded vertically. It also has additional space above it. I can make this stop, however, by setting the panel proportions as follows:

bar_flags = common_flags.Proportion(-1)

main_box.Add(toolbar1, bar_flags)
main_box.Add(wrap_panels_box, common_flags.Proportion(0))
main_box.Add(toolbar2, bar_flags)
main_box.Add(statusbar, bar_flags)

A -1 value isn't even documented for the Proportion()[note 2] setting on a Sizer, and the behavior basically matches what I would expect from the original code sample. What's going on here?

2. Later elements in BoxSizer sliding over earlier elements

Regardless of how I have the proportions set (at least between the two options above), the latter items behave as expected with relation to each other. They slide over the first element if the box becomes small, however. So, if I have _GenerateViewPanels() return the panels (as usual), they slide up and cover the top toolbar. If I make that do nothing (none of the normal panels are generated), the next toolbar slides up and covers the top toolbar. To reiterate: none of the bottom toolbars or panels interact with each other that way; they only do it with the first toolbar. As before, I'm confused: what's going on here?

[Edit: Made the above code a fully working sample application.]


Notes:

  1. I'm using a wxPython snapshot build of Phoenix, specifically 2.9.5.81-r73784, running against Python 3.3 on both Windows 7 and OS X. It is possible this is a problem from the snapshot build, but I'm doubtful.
  2. Proportion() is a fancy wrapper for the basic proportion=<value> argument for adding an element to a Sizer. As for why I tried -1, I just mis-remembered the default/base values for the proportion argument to BoxSizer.Add().

Solution

  • The problem is apparently with the distinct Proportion() calls. A little testing and use of the wxPython Widget Inspection Tool makes clear that common_flags is being modified by the calls on it.

    All methods of SizerFlags objects return the same object (not a copy of the object), so calling a method updates the object and all references to it – it does not return a copy, but the same object. So, the original code with comments added explaining what went wrong:

    common_flags = wx.SizerFlags().Expand().Border(wx.ALL, 1)  # creates the object
    bar_flags = common_flags.Proportion(0)  # bar_flags points to the common_flags object
    
    # Referencing common_flags with Proportion set to 0
    main_box.Add(toolbar1, bar_flags)
    
    # Changes the value of common_flags.
    main_box.Add(wrap_panels_box, common_flags.Proportion(1))
    
    # Since bar_flags points to common_flags, it also has Proportion set to 1
    main_box.Add(toolbar2, bar_flags)
    main_box.Add(statusbar, bar_flags)
    

    The solution is simple: declare bar_flags and box_flags as separate objects. This involves some small repetition of code, but it's worth note that you are not repeating the action on the same object; you are performing the same actions on multiple objects. Supplying the following code instead solves the issue:

    bar_flags = wx.SizerFlags().Expand().Border(wx.ALL, 1).Proportion(0)
    box_flags = wx.SizerFlags().Expand().Border(wx.ALL, 1).Proportion(1)
    
    main_box.Add(tool_status_bars.main, bar_flags)
    main_box.Add(wrap_panels_box, box_flags)
    main_box.Add(tool_status_bars.panel_view, bar_flags)
    main_box.Add(tool_status_bars.status, bar_flags)
    

    As expected, the boxes now relate to each other as they should: the wrap_panels_box expands, while the tool and status bars do not.