GUI Design (Code) Question

So I’m fiddling around with an app, and it occurred to me that I’m missing out on something. Okay, I miss out on stuff all of the time, but bear with me.

If I have an application with the following UI:

What is considered the best practice for this UI?

  1. Embed two side-by-side panels in the wxFrame
  2. Put another panel on the wxFrame and embed the two panels on it
  3. Subclass wx.Panel and embed all of the controls on it using sizers to control placement

I’ve got a version of each because I’m learning/experimenting, but my thoughts are:

  1. May be overkill when #3 may suffice.
  2. Seems like this would use additional system resources unnecessarily.
  3. BFFI version. :joy:

I’m going to be teaching my son and some of his mates how to design GUIs this summer and wxPython is sort of my preferred toolkit soooooo…

All thoughts, comments, etc. are welcome!

Why make things complicated with a lot of panels?

  • A frame with a panel. Then a horizontal box sizer for left/right.
  • A vertical box sizer for the texts and headings at the left.
  • A vertical box sizer for the right half. A horizontal box sizer for the three buttons.

Depending on the required re-sizing behaviour, a grid sizer with a single column might be required for the left half (to distribute the controls over the full height).
For the right half, a static box sizer (i.e. with a label) might be applicable.

ChuckM.py (4.1 KB)
ChuckM.zip (1.2 KB)

To quote myself from http://wxglade.sourceforge.net/docs/wxbasics.html :

  • For your own projects, always use the simplest available sizers. Usually you will need mainly box sizers and maybe one or two FlexGridSizers.
  • Use nested sizers to match the hierarchical / logical structure of your project. This will make it easy to re-arrange things to find the best user interface.
  • Never ever try to use a GridBagSizer as main sizer of a window trying to resemble pixel placement or Tkinter’s grid geometry manager. This is a mess to create and maintain. Actually, a GridBagSizer is almost never needed.

Hi.

I do agree with DietmarSchwertberfer : no needs of multiple panels for such an interface.
And the method he describes will facilitate yourself if you have (for example) to fill the textboxes when selecting an entry in the list on the right.

Regards
Xav’

Thanks for your response. That’s the approach I’ve taken on a personal project, but I’ve been away from wxWidgets/wxPython for a while and as I’m just getting back into the swing I like to know what the current set of best practices are from people who are working with the toolkit every day.

The other side of the coin is that sometimes it does make sense to use the extra panels. It’s really not enough overhead to matter, for a reasonable number of widgets anyway. For example, if some group of widgets are a self-contained concept, then encapsulating all the UI and at least some of the behavior into a single class derived from wx.Panel makes sense. It really depends on what makes the most sense to you, and how you prefer to organize things.

trying to generate a GUI it’s obvious to fall back to the minimum: h & v boxes are the bare bones (after the panel); but what the user scribbles usually resembles what the GridBag wants to provide! so coming from the application side one wouldn’t dream of less than a GridBag (per panel): after all most screens are still squarish filled with some controls here & there…

I have a question, why the use of a panel anyway, at least in this kind of examples?
I would like to know what its advantage is. wxGlade by default adds a Panel with a Sizer to the frame (unless you uncheck that option, but I get the impression that you can as well add the Sizer directly to the Frame. Is there a disadvantage to doing this?
For example, I took the ChuckM example above and removed the Panel end sizer_1, and it gives the same UI.

On some platforms, I think mainly Windows, the panel is responsible for the navigation through the controls. Without the panel you can’t navigate through the controls with Tab and Shift+Tab.

Also on Windows the background colour of the plain frame will look darker than usual.

(The release versions of wxGlade also require a sizer between the frame and the panel. This has changed in the master branch already.)

2 Likes

it was too tempting…ChuckM - Copy.py (2.6 KB)

Nice, I haven’t played with the GridSizer yet although I will likely get to it in a later iteration.

I’m currently focused on the widgets to work the way I need them to. At this current point in the project, I want to keep things somewhat simple so that I can see what’s going on, but I know from experience that I will continue to iterate to find better ways to do what I’m doing. :grin:

I’m a big fan of using wxGlade to keep my GUI development separate from the rest of the project. And I find that I can do most things within wxGlade with only a smattering of hand wrapping when I want to be really fancy.

That said, I find that having widgets grouped in a Panel lets me Hide the panel/widgets later (if I want to make those controls unavailable for some reason). Just my 2 cents.

1 Like

I like using a Dialog window for windows that look like that so that it can stand alone as an app, or be reused later as part of a larger project, if appropriate.

app.py (578 Bytes) chuckm_dialog.zip (2.4 KB) MyDialog.py (3.5 KB)

another Roman outing…
ChuckM - Copy.py (2.6 KB)
and I can assure you the most interesting quarters (listview on this trip) we haven’t been yet…

You can also use wxFormBuilder to quickly construct your UI design.

the window was crying out lout for a split…
ChuckM_split.py (2.7 KB)

and hiding/showing things (a bit like a smartphone) is easy with sizers!
here the buttons hide when you swipe to the bottom (status bar actually) and come up when touching the list again…
ChuckM_od.py (3.2 KB)

or just put a little grip into a convenient spot…

ChuckM_od_grp.py (3.2 KB)

and, similarly, one may put some home-grown tooltip on the table columns…

import wx

GS = 'Do in', 'Rome', 'as', 'the', 'Romans', 'do'
BTN = ('button_1', 1), ('button_2', 0), ('button_3', 1)
COLUMNS = 'Label', 'Value'

class Gui(wx.Frame):
    def __init__(self, parent):
        super().__init__(parent, title='never repeat..')
        self.frame_statusbar = self.CreateStatusBar()

        vb = wx.BoxSizer(wx.VERTICAL)
        panel = wx.Panel(self, wx.ID_ANY)
        vb.Add(panel, 1, wx.EXPAND, 8)
        self.SetSizer(vb)

        # split horizontally
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        splitter = wx.SplitterWindow(
            panel, wx.ID_ANY,
            style=wx.SP_LIVE_UPDATE | wx.SP_THIN_SASH)
        hbox.Add(splitter, 1, wx.EXPAND)
        panel.SetSizer(hbox)

        panel1 = wx.Panel(splitter, wx.ID_ANY)
        panel2 = wx.Panel(splitter, wx.ID_ANY)
        splitter.SplitVertically(panel1, panel2, 500)
        splitter.SetMinimumPaneSize(50)  # no window should disappear

        # text lines on the left
        vbox = wx.BoxSizer(wx.VERTICAL)

        gs = wx.GridSizer(1)
        self.gs_ctrl = dict()                                                     # ctrl: row
        self.gs_row = dict()                                                     # row: ctrl
        for idx, entry in enumerate(GS):
            gs.Add(wx.StaticText(panel1, wx.ID_ANY, entry))
            tc = wx.TextCtrl(panel1)
            self.gs_ctrl[tc.GetId()] = idx
            self.gs_row[idx] = tc
            gs.Add(tc, 0, wx.EXPAND)
        vbox.Add(gs, 0, wx.EXPAND|wx.ALL, 8)
        panel1.SetSizer(vbox)

        # list on the right
        vbox = wx.BoxSizer(wx.VERTICAL)

        hdr = wx.StaticText(panel2, wx.ID_ANY, "My Stuff")        # header

        self.list = wx.ListView(panel2)                                        # list
        for col in COLUMNS:
            self.list.AppendColumn(col)
        self.list.Bind(wx.EVT_LIST_COL_DRAGGING, self.evt_col_width)
        self.list.Bind(wx.EVT_SCROLLWIN_THUMBRELEASE, self.evt_scroll)

        grp = wx.Window(panel2, size=(0, 1))                    # buttons grip
        grp.SetBackgroundColour(wx.BLUE)
        grp.Bind(wx.EVT_ENTER_WINDOW, self.grip_btns)

        hbox = wx.BoxSizer(wx.HORIZONTAL)                          # buttons
        self.btn_ctrl = dict()                                                     # ctrl: pos
        self.ctrl_btn = dict()                                                     # pos: ctrl
        for idx, entry in enumerate(BTN):
            btn = wx.Button(panel2, -1, entry[0])
            hbox.Add(btn, entry[1], wx.EXPAND)
            self.btn_ctrl[btn.GetId()] = idx
            self.ctrl_btn[idx] = btn.GetId()

        htt = self.update_htt(panel2)                       # header tooltips

        self.fgs = wx.FlexGridSizer(1)
        self.fgs.AddMany(
            [hdr, (htt, 0, wx.EXPAND), (self.list, 0, wx.EXPAND),
            (grp, 0, wx.EXPAND), (hbox, 0, wx.EXPAND)])
        self.fgs.AddGrowableCol(0, 0)
        self.fgs.AddGrowableRow(2, 0)
        vbox.Add(self.fgs, 1, wx.EXPAND|wx.ALL, 8)
        panel2.SetSizerAndFit(vbox)

        vb.Fit(self)
        self.grip_btns(None)
        self.Show()

    def evt_col_width(self, _, /):
        self.update_htt()
        self.fgs.Layout()

    def evt_scroll(self, evt, /):
        if evt.GetPosition():
            self.list.Unbind(wx.EVT_LIST_COL_DRAGGING)
            self.fgs.Hide(1)
        else:
            self.list.Bind(wx.EVT_LIST_COL_DRAGGING, self.evt_col_width)
            self.fgs.Show(1)
        evt.Skip()

    def grip_btns(self, _, /):
        if self.fgs.IsShown(4):
            self.fgs.Hide(4)
        else:
            self.fgs.Show(4)
        self.fgs.Layout()

    def update_htt(self, panel=None, /):
        if not panel:
            hbox = self.fgs.GetChildren()[1].GetSizer()
            panel = hbox.GetChildren()[0].GetWindow().GetParent()
            hbox.Clear(delete_windows=True)
        else:
            hbox = wx.BoxSizer(wx.HORIZONTAL)
        for col, entry in enumerate(COLUMNS):
            win = wx.Window(
                panel, -1, size=(self.list.GetColumnWidth(col), 3))
            hbox.Add(win)
            win.SetToolTip(f'tip for column "{entry}"')
            win.SetBackgroundColour('light blue')
        return hbox

if __name__ == "__main__":
    app = wx.App()
    app.TopWindow = Gui(None)
    app.MainLoop()type or paste code here

there was a line missing…

ChuckM_od_grp_htt.py (4.4 KB)

and to care for changing column order as well: the extra sill is for a missing event, but with the proper sizer it’s fun to be a teeny-weeny bit creative…

ChuckM_od_grp_htt.py (4.7 KB)

and if you want to browse big data (until the mouse screams)

ChuckM_scrwin.py (10.5 KB)

1 Like

so the header of a scrolled window is really a customized box sizer

ChuckM_scrwin.py (10.2 KB)

and an on demand quick finder always comes in handy

ChuckM_scrwin_blth.py (12.3 KB)

and using proper sizers makes it easy to separate the dumb view from the less dumb controller…

ChuckM_view.py (6.8 KB)
ChuckM_ctrl.py (6.3 KB)

and to stay structurally clean every main sizer should handle its own events (I think)

ChuckM_view.py (6.6 KB)
ChuckM_gs_eh.py (404 Bytes)
ChuckM_fgs_eh.py (6.5 KB)

and occasionally (when things go wrong?) one likes to see the printouts (my favourite is

    def __del__(self):
        print(f'{id(self)=} {self.__class__}: __del__')

to see whether the gc has been): just dclick the status bar…

ChuckM_gs_eh.py (2.3 KB)