Preventing wxPython widget collisions

02 Apr 2019

This is Part 3. In Part 1 we wrote a Cassowary demo in Python using the kiwisolver package. In Part 2 we used kiwisolver to handle a wxPython layout. Now we’ll add a way to detect and handle widget collisions when the frame gets resized too small.

Playing around with the example code in Part 2, you can see the buttons can overlap each other if you shrink the frame too much. A discussion on stackoverflow shows one way to handle this problem.

The overlap() function returns True if two widgets touch each other. If so the on_resize() event handler sets the frame minimum size, preventing further shrinkage.

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 overlap_all(widgets):
    while len(widgets) > 1:
        widget = widgets.pop()
        for item in widgets:
            if overlap(widget, item):
                return True
    return False

def value_in_range(value, min, max):
    return (value >= min) & (value <= max)

def overlap(a, b):
    x_overlap = value_in_range(
        a.x.value(),
        b.x.value(),
        b.x.value() +
        b.width.value()) | value_in_range(
        b.x.value(),
        a.x.value(),
        a.x.value() +
        a.width.value())
    y_overlap = value_in_range(
        a.y.value(),
        b.y.value(),
        b.y.value() +
        b.height.value()) | value_in_range(
        b.y.value(),
        a.y.value(),
        a.y.value() +
        a.height.value())
    return x_overlap & y_overlap

def on_resize(event):
    width, height = frame.GetSize()
    solver.suggestValue(wframe.width, width)
    solver.suggestValue(wframe.height, height)
    solver.updateVariables()
    if overlap_all(list(db.values())):
        frame.SetSizeHints(minW=width, minH=height)
    else:
        # Turn off minimum size if no overlap.
        min_size = frame.GetMinSize()
        if min_size[0] != -1 or min_size[1] != -1:
            frame.SetSizeHints(minW=-1, minH=-1)
    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():
    b1width, b1height = button1.GetBestSize()
    b2width, b2height = button2.GetBestSize()
    constraints = [
        db[textbox].x == 10,
        db[textbox].y == 10,
        db[textbox].width == wframe.width - 20,
        db[textbox].height == wframe.height - 100,
        db[button1].width == b1width,
        db[button1].height == b1height,
        db[button2].width == b2width,
        db[button2].height == b2height,
        db[button1].x == db[textbox].x,
        db[button1].y == db[textbox].y + db[textbox].height + 25,
        db[button2].x == db[textbox].x + db[textbox].width - db[button2].width,
        db[button2].y == db[button1].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()