wxPython using the Cassowary layout algorithm

26 Mar 2019

This is Part 2. In Part 1 we wrote a Cassowary demo in Python using the kiwisolver package. Now we’ll apply what we learned by using kiwisolver to handle a wxPython layout.

In brief we need to:

  1. Create a wx.App() object.
  2. Create our wxPython objects.
  3. Create a kiwisolver.Solver() object.
  4. Create a Widget object for each wxPython object. The Widget is pretty simple: it just stores x, y, width, and height in a kiwisolver.Variable.
  5. Create a dictionary to store each Widget object with its wxPython object.
  6. Create our constraints: each constraint is a linear equation which describes how one object relates to another.
  7. Call addConstraint() to add each constraint to the Solver.
  8. The wx.Frame is set using addEditVariable() because it will be our input (the system will take in a new frame size and output the changes).
  9. The on_resize() event handler is called every time the frame is resized.
    1. Set the wframe.width and height variables to the wx.Frame’s new size.
    2. Call updateVariables() to perform the calculations on each Widget’s x, y, width, and height.
    3. Update each wx object with its new x, y, width, and height.

Python code

import kiwisolver
import wx

class Widget(object):
    def __init__(self, identifier):
        self.x = kiwisolver.Variable('x-' + identifier)
        self.y = kiwisolver.Variable('y-' + identifier)
        self.width = kiwisolver.Variable('width-' + identifier)
        self.height = kiwisolver.Variable('height-' + identifier)

def on_resize(event):
    width, height = frame.GetSize()
    solver.suggestValue(wframe.width, width)
    solver.suggestValue(wframe.height, height)
    solver.updateVariables()
    for obj in db:
        widget = db[obj]
        obj.SetSize(
            x=widget.x.value(),
            y=widget.y.value(),
            width=widget.width.value(),
            height=widget.height.value())
    event.Skip() # Allow default event handling.

def create_constraints():
    box = db[textbox]
    b1 = db[button1]
    b2 = db[button2]
    constraints = [
        box.x == 10,
        box.y == 10,
        box.width == wframe.width - 20,
        box.height == wframe.height - 100,
        b1.width == button1.GetBestSize()[0],
        b1.height == button1.GetBestSize()[1],
        b2.width == button2.GetBestSize()[0],
        b2.height == button2.GetBestSize()[1],
        b1.x == box.x,
        b1.y == box.y + box.height + 25,
        b2.x == box.x + box.width - b2.width,
        b2.y == b1.y,
    ]
    for constraint in constraints:
        solver.addConstraint(constraint)
    solver.addEditVariable(wframe.width, 'strong')
    solver.addEditVariable(wframe.height, 'strong')

if __name__ == '__main__':
    app = wx.App()
    frame = wx.Frame(
        parent=None,
        id=wx.ID_ANY,
        title='Cassowary Demo')
    panel = wx.Panel(parent=frame, id=wx.ID_ANY)
    textbox = wx.TextCtrl(
        parent=panel,
        id=wx.ID_ANY,
        value='When in the course of human events...',
        style=wx.TE_MULTILINE)
    button1 = wx.Button(
        parent=panel,
        id=wx.ID_ANY,
        label='Button1')
    button2 = wx.Button(
        parent=panel,
        id=wx.ID_ANY,
        label='Button2')

    wframe = Widget('frame')
    db = {}
    db[textbox] = Widget('textbox')
    db[button1] = Widget('button1')
    db[button2] = Widget('button2')
    solver = kiwisolver.Solver()
    create_constraints()
    frame.Bind(wx.EVT_SIZE, on_resize)
    frame.Show()
    app.MainLoop()