Search code examples
pythonwxwidgetsboxsizer

wx python BoxSizer not expanding to window plus an odd blank gap at top of GUI


I am having some issues properly expanding my sizers. I have added two sub panels which I was hoping would solve my issue, but it seems to have made it worse. What I am hoping to do is have top_panel expand to the extents of the width of the main panel. vbox_top_right should expand to fill the rest of hbox_top. The bottom_panel should then expand both horizontally and vertically to fill in the rest of the space of the main panel. None of this is happening.

Once I split everything up into two sub panels, I am now having an issues where I have a large blank space up top. Also, my bottom_panel does not seem to be expanding anywhere. I am also not sure if I am using SetSizerAndFit correctly because I haven't seen anywhere mentioning the use of it on multiple panels. Am I only supposed to be applying it to the main panel?

Note that I am forcing use of wx 3.0. I have 2.8 and 3.0 installed and had issues with wx.StaticBoxSizer with 2.8. I know this likely has nothing to do with my issue, just the code won't work correctly if you are trying it in 2.8.

CODE:

import wxversion
wxversion.select('3.0')
import wx
import os
import sys


VERSION = '1.0.0'


class GUI(wx.Frame):

    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, id=wx.ID_ANY, title=title, pos=wx.DefaultPosition,
                          size=wx.Size(1280, 768), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
        menu_bar = wx.MenuBar()
        file_menu = wx.Menu()
        self.cwd = os.getcwd()

        # Quit code
        file_item = file_menu.Append(wx.ID_EXIT, 'Quit', 'Quit Application')
        menu_bar.Append(file_menu, '&File')
        self.SetMenuBar(menu_bar)

        # Add Main panel
        self.panel = wx.Panel(self)

        # Add Top and Bottom Panels
        self.top_panel = wx.Panel(self.panel)
        self.bottom_panel = wx.Panel(self.panel)


        # Create horizontal and vertical boxes
        self.hbox_main = wx.BoxSizer(wx.HORIZONTAL)
        self.vbox_main = wx.BoxSizer(wx.VERTICAL)
        self.hbox_top = wx.BoxSizer(wx.HORIZONTAL)
        self.vbox_top_left = wx.BoxSizer(wx.VERTICAL)
        self.vbox_top_right = wx.BoxSizer(wx.VERTICAL)

        ####################################################
        # TOP
        ####################################################

        ##########################
        # TOP LEFT
        ##########################
        # List box text
        self.lbl_filter = wx.StaticText(self.top_panel, wx.ID_ANY,
                                        u"Select all cases to apply file to",
                                        wx.DefaultPosition, wx.DefaultSize, 0)
        self.lbl_filter.Wrap(-1)
        self.vbox_top_left.Add(self.lbl_filter, 0, wx.EXPAND)

        # The list box that all the file names are in
        self.list_box = wx.CheckListBox(self.top_panel, id=wx.ID_ANY, pos=wx.DefaultPosition, size=(300, 300),
                                        choices=[], style=wx.LB_HSCROLL|wx.LB_MULTIPLE|wx.LB_NEEDED_SB|wx.LB_SORT)
        self.vbox_top_left.Add(self.list_box, 0, wx.EXPAND, 5)

        # List box filter text
        self.lbl_filter = wx.StaticText(self.top_panel, wx.ID_ANY,
                                        u"Case filter (separate wildcards with a comma and choose filter logic (AND or OR)\n(i.e. HS, 2022 with AND selected will modify all 2022 HS cases)",
                                        wx.DefaultPosition, wx.DefaultSize, 0)
        self.lbl_filter.Wrap(-1)
        self.vbox_top_left.Add(self.lbl_filter, 0, wx.EXPAND)

        # List box filter
        self.hbox_filter = wx.BoxSizer(wx.HORIZONTAL)
        self.txt_filter = wx.TextCtrl(self.top_panel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0)
        self.txt_filter.SetMinSize(wx.Size(300, -1))
        self.hbox_filter.Add(self.txt_filter, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND, 5)

        # List box radio buttons
        logic_choices = [u"AND", u"OR"]
        self.rdo_logic = wx.RadioBox(self.top_panel, wx.ID_ANY, u"Filter Logic", wx.DefaultPosition, wx.DefaultSize,
                                     logic_choices, 1, wx.RA_SPECIFY_ROWS)
        self.rdo_logic.SetSelection(0)
        self.hbox_filter.Add(self.rdo_logic, 0, wx.EXPAND, 5)

        # Add filter stuff to vbox_top
        self.vbox_top_left.Add(self.hbox_filter, 0, wx.EXPAND)

        # Add top components to hbox
        self.hbox_top.Add(self.vbox_top_left, 0, wx.EXPAND)


        ##########################
        # TOP RIGHT
        ##########################
        # Add warning text
        self.lbl_warning = wx.StaticText(self.top_panel, wx.ID_ANY,
                                         u"*** WARNING *** HELLO WORLD ",
                                         wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTER_HORIZONTAL)
        self.lbl_warning.Wrap(-1)
        self.vbox_top_right.Add(self.lbl_warning, 0, wx.EXPAND, 5)

        # Radio buttons for software choice
        rdo_choices = [u"App 1", u"App 2", u"App 3"]
        self.rdo_software = wx.RadioBox(self.top_panel, wx.ID_ANY, u"Select Software", wx.DefaultPosition,
                                        wx.DefaultSize, rdo_choices, 1, wx.RA_SPECIFY_COLS)
        self.rdo_software.SetSelection(0)
        self.vbox_top_right.Add(self.rdo_software, 0, wx.EXPAND, 5)

        # Checkbox for archive
        self.cb_archive = wx.CheckBox(self.top_panel, wx.ID_ANY, u"Archive files before running", wx.DefaultPosition,
                                      wx.DefaultSize, 0)
        self.vbox_top_right.Add(self.cb_archive, 0, wx.EXPAND, 5)

        # Checkbox for saving
        self.cb_save = wx.CheckBox(self.top_panel, wx.ID_ANY, u"Save files after running", wx.DefaultPosition,
                                   wx.DefaultSize, 0)
        self.vbox_top_right.Add(self.cb_save, 0, wx.EXPAND, 5)

        # Folder selection label
        self.lbl_cases = wx.StaticText(self.top_panel, wx.ID_ANY, u"Select Folder With Cases", wx.DefaultPosition,
                                       wx.DefaultSize, 0)
        self.lbl_cases.Wrap(-1)
        self.vbox_top_right.Add(self.lbl_cases, 0, wx.EXPAND, 5)

        # Add Folder selection
        self.hbox_folder = wx.BoxSizer(wx.HORIZONTAL)
        self.txt_cases = wx.TextCtrl(self.top_panel, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0)
        self.txt_cases.SetMinSize(wx.Size(400, -1))
        self.hbox_folder.Add(self.txt_cases, 0, wx.EXPAND, 5)

        # Folder select button
        self.btn_cases = wx.Button(self.top_panel, wx.ID_ANY, u"Case Folder", wx.DefaultPosition, wx.DefaultSize, 0)
        self.hbox_folder.Add(self.btn_cases, 0, wx.EXPAND, 5)

        # Add to sizer
        self.vbox_top_right.Add(self.hbox_folder, 1, wx.SHAPED, 5)
        self.hbox_top.Add(self.vbox_top_right, 0, wx.EXPAND, 5)
        self.vbox_main.Add(self.hbox_top, 0, wx.EXPAND)

        ####################################################
        # BOTTOM
        ####################################################

        self.bottom_box = wx.StaticBox(self.bottom_panel, label='Progress Output')
        self.hbox_output = wx.StaticBoxSizer(self.bottom_box, wx.HORIZONTAL)
        self.txt_output = wx.TextCtrl(self.bottom_box, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize,
                                      wx.TE_MULTILINE | wx.TE_READONLY)


        # Put it all together
        self.vbox_main.Add(self.top_panel, 1, wx.EXPAND)
        self.vbox_main.Add(self.bottom_panel, 1, wx.EXPAND)
        self.top_panel.SetSizerAndFit(self.hbox_top)
        self.bottom_panel.SetSizerAndFit(self.hbox_output)
        self.panel.SetSizerAndFit(self.vbox_main)
        self.Centre()
        self.Layout()


if __name__ == '__main__':
    app = wx.App(0)
    MainFrame = GUI(None, title='Batch Apply %s' % VERSION)
    app.SetTopWindow(MainFrame)
    MainFrame.Show()
    app.MainLoop()

Here is a crude drawing of the GUI Crude drawing of GUI

Here is what I am getting: Output


Solution

  • The main frame has only one child called panel. So, this only child of a frame (not of another kind of window) will fit it's parent's client area when it gets resized. Good. It would'nt be true in case of more than one child.

    You have two more panels, as children of panel. This would be good if, for example, you want different background colors. If not, that's not necessary, but there's nothing wrong with these superfluous panels.

    There are two different regions: top (not resizable in vertical direction) and bottom (resizable). This would require two sub-sizers of the 'main sizer' that handles the layout of the contents of panel. But you are using two panels for these regions, so better let's use a main sizer (I'll use vbox_main) for the layout inside panel of these two panels. The layout of the contents of a sub-panel will be handled by a sizer.

    In the 'top' region there are also two different areas; so another two sub-sizers. Your design is correct.

    vbox_top_left wants the controls it manages (children of top_panel) to fit in the available space. Because this is a vertical sizer we need:
    1) A child can change vertical size: use proportion=1
    2) A child can change horizontal size: use wx.EXPAND flag.
    3) If would be good to add vertical resizable spacers between some controls.

    Apply similar criteria for the controls size-handled by vbox_top_right (with children of top_panel too).

    Bottom area is peculiar because you want a rectangle to be drawn around a label and a text control. For this, we need a special sizer: StaticBoxSizer. It's peculiar in the sense that the controls it handles are children not of a panel, but of the underlaying wx.StaticBox. See the upper docs link for an example and more explanations.

    self.hbox_output = wx.StaticBoxSizer(wx.HORIZONTAL, self.bottom_panel)
    self.hbox_output.Add(wx.StaticText(self.hbox_output.GetStaticBox(), ....), ...)
    self.hbox_output.Add(wx.TextCtrl(self.hbox_output.GetStaticBox(), ....), 1, wx.EXPAND, 5)
    


    To add children to this sizer follow the same criteria as for 'top' area. There's nothing distinct here.

    Now, behaviours of the sizers (I skip other sub-sizers you use):

    # No vertical nor horizontal expanding
    # self.hbox_top.Add(self.vbox_top_left, 0, wx.EXPAND) <<== not what expected
    self.hbox_top.Add(self.vbox_top_left, 0)
    
    # Only horizontal expanding
    # self.hbox_top.Add(self.vbox_top_right, 0, wx.EXPAND, 5)  <<== not what expected
    self.hbox_top.Add(self.vbox_top_right, 1, 0, 5)
    

    The last job is to bind panels and sizers:

    self.top_panel.SetSizer(self.hbox_top)
    self.bottom_panel.SetSizer(self.hbox_output)
    
    # Top panel expands only in horizontal
    self.vbox_main.Add(self.top_panel, 0, wx.EXPAND)
    # Bottom part expands in both directions
    self.vbox_main.Add(self.bottom_panel, 1, wx.EXPAND)
    
    self.panel.SetSizerAndFit(self.vbox_main)
    self.Centre()
    self.Layout()
    

    Using a single panel is easier. I've used your two sub-panels just to demostrate how it works: Main sizer handles sub-panels, each panel uses a sizer for its sizers that handle children.