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()