1
#----------------------------------------------------------------------
2
# Name: sized_controls.py
3
# Purpose: Implements default, HIG-compliant sizers under the hood
4
# and provides a simple interface for customizing those sizers.
6
# Author: Kevin Ollivier
9
# Copyright: (c) 2006 Kevin Ollivier
10
# Licence: wxWindows license
11
#----------------------------------------------------------------------
14
import wx.lib.scrolledpanel as sp
16
# For HIG info: links to all the HIGs can be found here:
17
# http://en.wikipedia.org/wiki/Human_Interface_Guidelines
20
# useful defines for sizer prop values
22
halign = { "left": wx.ALIGN_LEFT,
23
"center": wx.ALIGN_CENTER_HORIZONTAL,
24
"centre": wx.ALIGN_CENTRE_HORIZONTAL,
25
"right": wx.ALIGN_RIGHT,
28
valign = { "top": wx.ALIGN_TOP,
29
"bottom": wx.ALIGN_BOTTOM,
30
"center": wx.ALIGN_CENTER_VERTICAL,
31
"centre": wx.ALIGN_CENTRE_VERTICAL,
34
align = { "center": wx.ALIGN_CENTER,
35
"centre": wx.ALIGN_CENTRE,
38
border = { "left": wx.LEFT,
45
minsize = { "fixed": wx.FIXED_MINSIZE,
48
misc_flags = { "expand": wx.EXPAND, }
51
# My attempt at creating a more intuitive replacement for nesting box sizers
52
class TableSizer(wx.PySizer):
53
def __init__(self, rows=0, cols=0):
54
wx.PySizer.__init__(self)
65
# allow us to use 'old-style' proportions when emulating box sizers
66
self.isHorizontal = (self.rows == 1 and self.cols == 0)
67
self.isVertical = (self.cols == 1 and self.rows == 0)
69
def CalcNumRowsCols(self):
72
numchild = len(self.GetChildren())
74
if numrows == 0 and numcols == 0:
78
rows, mod = divmod(numchild, self.cols)
84
cols, mod = divmod(numchild, self.rows)
89
return numrows, numcols
92
numrows, numcols = self.CalcNumRowsCols()
93
numchild = len(self.GetChildren())
96
return wx.Size(10, 10)
98
if numrows == 0 and numcols == 0:
99
print "TableSizer must have the number of rows or columns set. Cannot continue."
100
return wx.Size(10, 10)
102
self.row_widths = [0 for x in range(0, numrows)]
103
self.col_heights = [0 for x in range(0, numcols)]
110
# get the max row width and max column height
111
for item in self.GetChildren():
113
currentRow, currentCol = divmod(counter, numcols)
115
currentCol, currentRow = divmod(counter, numrows)
118
width, height = item.CalcMin()
120
if self.isVertical and item.GetProportion() > 0:
121
self.hgrow += item.GetProportion()
122
elif self.isHorizontal and item.GetProportion() > 0:
123
self.vgrow += item.GetProportion()
125
if width > self.row_widths[currentRow]:
126
self.row_widths[currentRow] = width
128
if height > self.col_heights[currentCol]:
129
self.col_heights[currentCol] = height
134
for row_width in self.row_widths:
135
minwidth += row_width
138
for col_height in self.col_heights:
139
minheight += col_height
141
self.fixed_width = minwidth
142
self.fixed_height = minheight
144
return wx.Size(minwidth, minheight)
146
def RecalcSizes(self):
147
numrows, numcols = self.CalcNumRowsCols()
148
numchild = len(self.GetChildren())
156
print "cols %d, rows %d" % (self.cols, self.rows)
157
print "fixed_height %d, fixed_width %d" % (self.fixed_height, self.fixed_width)
158
#print "self.GetSize() = " + `self.GetSize()`
160
row_widths = [0 for x in range(0, numrows)]
161
col_heights = [0 for x in range(0, numcols)]
162
item_sizes = [0 for x in range(0, len(self.GetChildren()))]
163
grow_sizes = [0 for x in range(0, len(self.GetChildren()))]
169
# first, we set sizes for all children, and while doing so, calc
170
# the maximum row heights and col widths. Then, afterwards we handle
171
# the positioning of the controls
173
for item in self.GetChildren():
175
currentRow, currentCol = divmod(counter, numcols)
177
currentCol, currentRow = divmod(counter, numrows)
179
item_minsize = item.GetMinSizeWithBorder()
180
width = item_minsize[0]
181
height = item_minsize[1]
183
print "row_height %d, row_width %d" % (self.col_heights[currentCol], self.row_widths[currentRow])
184
growable_width = (self.GetSize()[0]) - width
185
growable_height = (self.GetSize()[1]) - height
187
#if not self.isVertical and not self.isHorizontal:
188
# growable_width = self.GetSize()[0] - self.row_widths[currentRow]
189
# growable_height = self.GetSize()[1] - self.col_heights[currentCol]
191
#print "grow_height %d, grow_width %d" % (growable_height, growable_width)
195
# support wx.EXPAND for box sizers to be compatible
196
if item.GetFlag() & wx.EXPAND:
198
if self.hgrow > 0 and item.GetProportion() > 0:
199
item_hgrow = (growable_width * item.GetProportion()) / self.hgrow
200
item_vgrow = growable_height
202
elif self.isHorizontal:
203
if self.vgrow > 0 and item.GetProportion() > 0:
204
item_vgrow = (growable_height * item.GetProportion()) / self.vgrow
205
item_hgrow = growable_width
207
if growable_width > 0 and item.GetHGrow() > 0:
208
item_hgrow = (growable_width * item.GetHGrow()) / 100
209
print "hgrow = %d" % (item_hgrow)
211
if growable_height > 0 and item.GetVGrow() > 0:
212
item_vgrow = (growable_height * item.GetVGrow()) / 100
213
print "vgrow = %d" % (item_vgrow)
215
grow_size = wx.Size(item_hgrow, item_vgrow)
216
size = item_minsize #wx.Size(item_minsize[0] + item_hgrow, item_minsize[1] + item_vgrow)
217
if size[0] + grow_size[0] > row_widths[currentRow]:
218
row_widths[currentRow] = size[0] + grow_size[0]
219
if size[1] + grow_size[1] > col_heights[currentCol]:
220
col_heights[currentCol] = size[1] + grow_size[1]
222
grow_sizes[counter] = grow_size
223
item_sizes[counter] = size
228
for item in self.GetChildren():
230
currentRow, currentCol = divmod(counter, numcols)
232
currentCol, currentRow = divmod(counter, numrows)
234
itempos = self.GetPosition()
236
rowstart = itempos[0]
237
for row in range(0, currentRow):
238
rowstart += row_widths[row]
240
colstart = itempos[1]
241
for col in range(0, currentCol):
242
#print "numcols = %d, currentCol = %d, col = %d" % (numcols, currentCol, col)
243
colstart += col_heights[col]
245
itempos[0] += rowstart
246
itempos[1] += colstart
248
if item.GetFlag() & wx.ALIGN_RIGHT:
249
itempos[0] += (row_widths[currentRow] - item_sizes[counter][0])
250
elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL):
251
itempos[0] += (row_widths[currentRow] - item_sizes[counter][0]) / 2
253
if item.GetFlag() & wx.ALIGN_BOTTOM:
254
itempos[1] += (col_heights[currentCol] - item_sizes[counter][1])
255
elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL):
256
itempos[1] += (col_heights[currentCol] - item_sizes[counter][1]) / 2
258
hgrowth = (grow_sizes[counter][0] - itempos[0])
260
item_sizes[counter][0] += hgrowth
262
vgrowth = (grow_sizes[counter][1] - itempos[1])
264
item_sizes[counter][1] += vgrowth
265
#item_sizes[counter][1] -= itempos[1]
266
item.SetDimension(itempos, item_sizes[counter])
270
def GetDefaultBorder(self):
272
if wx.Platform == "__WXMAC__":
274
elif wx.Platform == "__WXMSW__":
275
# MSW HIGs use dialog units, not pixels
276
pnt = self.ConvertDialogPointToPixels(wx.Point(4, 4))
278
elif wx.Platform == "__WXGTK__":
283
def SetDefaultSizerProps(self):
284
item = self.GetParent().GetSizer().GetItem(self)
285
item.SetProportion(0)
287
item.SetBorder(self.GetDefaultHIGBorder())
289
def GetSizerProps(self):
291
Returns a dictionary of prop name + value
295
item = self.GetParent().GetSizer().GetItem(self)
299
props['proportion'] = item.GetProportion()
300
flags = item.GetFlag()
302
if flags & border['all'] == border['all']:
303
props['border'] = (['all'], item.GetBorder())
307
if flags & border[key]:
310
props['border'] = (borders, item.GetBorder())
312
if flags & align['center'] == align['center']:
313
props['align'] = 'center'
316
if flags & halign[key]:
317
props['halign'] = key
320
if flags & valign[key]:
321
props['valign'] = key
324
if flags & minsize[key]:
325
props['minsize'] = key
327
for key in misc_flags:
328
if flags & misc_flags[key]:
333
def SetSizerProp(self, prop, value):
335
Sets a sizer property
337
:param prop: valid strings are "proportion", "hgrow", "vgrow",
338
"align", "halign", "valign", "border", "minsize" and "expand"
339
:param value: corresponding value for the prop
343
sizer = self.GetParent().GetSizer()
344
item = sizer.GetItem(self)
345
flag = item.GetFlag()
346
if lprop == "proportion":
347
item.SetProportion(int(value))
348
elif lprop == "hgrow":
349
item.SetHGrow(int(value))
350
elif lprop == "vgrow":
351
item.SetVGrow(int(value))
352
elif lprop == "align":
353
flag = flag | align[value]
354
elif lprop == "halign":
355
flag = flag | halign[value]
356
elif lprop == "valign":
357
flag = flag | valign[value]
358
# elif lprop == "border":
359
# # this arg takes a tuple (dir, pixels)
360
# dirs, amount = value
364
# flag = flag | border[dir]
365
# item.SetBorder(amount)
366
elif lprop == "border":
367
# this arg takes a tuple (dir, pixels)
374
flag = flag | border[dir]
375
item.SetBorder(amount)
376
elif lprop == "minsize":
377
flag = flag | minsize[value]
378
elif lprop in misc_flags:
379
if not value or str(value) == "" or str(value).lower() == "false":
380
flag = flag &~ misc_flags[lprop]
382
flag = flag | misc_flags[lprop]
384
# auto-adjust growable rows/columns if expand or proportion is set
385
# on a sizer item in a FlexGridSizer
386
if lprop in ["expand", "proportion"] and isinstance(sizer, wx.FlexGridSizer):
387
cols = sizer.GetCols()
388
rows = sizer.GetRows()
389
# FIXME: I'd like to get the item index in the sizer instead, but
390
# doing sizer.GetChildren.index(item) always gives an error
391
itemnum = self.GetParent().GetChildren().index(self)
396
col, row = divmod( itemnum, rows )
398
row, col = divmod( itemnum, cols )
400
if lprop == "expand" and not sizer.IsColGrowable(col):
401
sizer.AddGrowableCol(col)
402
elif lprop == "proportion" and int(value) != 0 and not sizer.IsRowGrowable(row):
403
sizer.AddGrowableRow(row)
407
def SetSizerProps(self, props={}, **kwargs):
409
Allows to set multiple sizer properties
411
:param props: a dictionary of prop name + value
412
:param kwargs: key words can be used for properties, e.g. expand=True
416
allprops.update(props)
417
allprops.update(kwargs)
419
for prop in allprops:
420
self.SetSizerProp(prop, allprops[prop])
422
def GetDialogBorder(self):
424
if wx.Platform == "__WXMAC__" or wx.Platform == "__WXGTK__":
426
elif wx.Platform == "__WXMSW__":
427
pnt = self.ConvertDialogPointToPixels(wx.Point(7, 7))
432
def SetHGrow(self, proportion):
433
data = self.GetUserData()
435
data["HGrow"] = proportion
436
self.SetUserData(data)
439
if self.GetUserData() and "HGrow" in self.GetUserData():
440
return self.GetUserData()["HGrow"]
444
def SetVGrow(self, proportion):
445
data = self.GetUserData()
447
data["VGrow"] = proportion
448
self.SetUserData(data)
451
if self.GetUserData() and "VGrow" in self.GetUserData():
452
return self.GetUserData()["VGrow"]
456
def GetDefaultPanelBorder(self):
457
# child controls will handle their borders, so don't pad the panel.
460
# Why, Python?! Why do you make it so easy?! ;-)
461
wx.Dialog.GetDialogBorder = GetDialogBorder
462
wx.Panel.GetDefaultHIGBorder = GetDefaultPanelBorder
463
wx.Notebook.GetDefaultHIGBorder = GetDefaultPanelBorder
464
wx.SplitterWindow.GetDefaultHIGBorder = GetDefaultPanelBorder
466
wx.Window.GetDefaultHIGBorder = GetDefaultBorder
467
wx.Window.SetDefaultSizerProps = SetDefaultSizerProps
468
wx.Window.SetSizerProp = SetSizerProp
469
wx.Window.SetSizerProps = SetSizerProps
470
wx.Window.GetSizerProps = GetSizerProps
472
wx.SizerItem.SetHGrow = SetHGrow
473
wx.SizerItem.GetHGrow = GetHGrow
474
wx.SizerItem.SetVGrow = SetVGrow
475
wx.SizerItem.GetVGrow = GetVGrow
479
def AddChild(self, child):
480
# Note: The wx.LogNull is used here to suppress a log message
481
# on wxMSW that happens because when AddChild is called the
482
# widget's hwnd hasn't been set yet, so the GetWindowRect that
483
# happens as a result of sizer.Add (in wxSizerItem::SetWindow)
484
# fails. A better fix would be to defer this code somehow
485
# until after the child widget is fully constructed.
486
sizer = self.GetSizer()
488
item = sizer.Add(child)
490
item.SetUserData({"HGrow":0, "VGrow":0})
492
# Note: One problem is that the child class given to AddChild
493
# is the underlying wxWidgets control, not its Python subclass. So if
494
# you derive your own class, and override that class' GetDefaultBorder(),
495
# etc. methods, it will have no effect.
496
child.SetDefaultSizerProps()
498
def GetSizerType(self):
499
return self.sizerType
501
def SetSizerType(self, type, options={}):
503
Sets the sizer type and automatically re-assign any children
506
:param type: sizer type, valid values are "horizontal", "vertical",
507
"form", "table" and "grid"
508
:param options: dictionary of options depending on type
512
self.sizerType = type
513
if type == "horizontal":
514
sizer = wx.BoxSizer(wx.HORIZONTAL) # TableSizer(0, 1)
516
elif type == "vertical":
517
sizer = wx.BoxSizer(wx.VERTICAL) # TableSizer(1, 0)
520
#sizer = TableSizer(2, 0)
521
sizer = wx.FlexGridSizer(0, 2, 0, 0)
522
#sizer.AddGrowableCol(1)
524
elif type == "table":
526
if options.has_key('rows'):
527
rows = int(options['rows'])
529
if options.has_key('cols'):
530
cols = int(options['cols'])
532
sizer = TableSizer(rows, cols)
535
sizer = wx.FlexGridSizer(0, 0, 0, 0)
536
if options.has_key('rows'):
537
sizer.SetRows(int(options['rows']))
540
if options.has_key('cols'):
541
sizer.SetCols(int(options['cols']))
545
if options.has_key('growable_row'):
546
row, proportion = options['growable_row']
547
sizer.SetGrowableRow(row, proportion)
549
if options.has_key('growable_col'):
550
col, proportion = options['growable_col']
551
sizer.SetGrowableCol(col, proportion)
553
if options.has_key('hgap'):
554
sizer.SetHGap(options['hgap'])
556
if options.has_key('vgap'):
557
sizer.SetVGap(options['vgap'])
559
self._SetNewSizer(sizer)
561
def _DetachFromSizer(self, sizer):
563
for child in self.GetChildren():
564
# On the Mac the scrollbars and corner gripper of a
565
# ScrolledWindow will be in the list of children, but
566
# should not be managed by a sizer. So if there is a
567
# child that is not in a sizer make sure we don't track
568
# info for it nor add it to the next sizer.
569
csp = child.GetSizerProps()
571
props[child.GetId()] = csp
572
self.GetSizer().Detach(child)
576
def _AddToNewSizer(self, sizer, props):
577
for child in self.GetChildren():
578
csp = props.get(child.GetId(), None)
579
# See Mac comment above.
581
self.GetSizer().Add(child)
582
child.SetSizerProps(csp)
585
class SizedPanel(wx.PyPanel, SizedParent):
586
def __init__(self, *args, **kwargs):
590
Controls added to it will automatically be added to its sizer.
593
'self' is a SizedPanel instance
595
self.SetSizerType("horizontal")
597
b1 = wx.Button(self, wx.ID_ANY)
598
t1 = wx.TextCtrl(self, -1)
599
t1.SetSizerProps(expand=True)
602
wx.PyPanel.__init__(self, *args, **kwargs)
603
sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0)
605
self.sizerType = "vertical"
607
def AddChild(self, child):
609
Called automatically by wx, do not call it from user code
612
if wx.VERSION < (2,8):
613
wx.PyPanel.base_AddChild(self, child)
615
wx.PyPanel.AddChild(self, child)
617
SizedParent.AddChild(self, child)
619
def _SetNewSizer(self, sizer):
620
props = self._DetachFromSizer(sizer)
621
wx.PyPanel.SetSizer(self, sizer)
622
self._AddToNewSizer(sizer, props)
625
class SizedScrolledPanel(sp.ScrolledPanel, SizedParent):
626
def __init__(self, *args, **kwargs):
627
"""A sized scrolled panel
629
Controls added to it will automatically be added to its sizer.
632
'self' is a SizedScrolledPanel instance
634
self.SetSizerType("horizontal")
636
b1 = wx.Button(self, wx.ID_ANY)
637
t1 = wx.TextCtrl(self, -1)
638
t1.SetSizerProps(expand=True)
641
sp.ScrolledPanel.__init__(self, *args, **kwargs)
642
sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0)
644
self.sizerType = "vertical"
645
self.SetupScrolling()
647
def AddChild(self, child):
649
Called automatically by wx, should not be called from user code
652
if wx.VERSION < (2,8):
653
sp.ScrolledPanel.base_AddChild(self, child)
655
sp.ScrolledPanel.AddChild(self, child)
657
SizedParent.AddChild(self, child)
659
def _SetNewSizer(self, sizer):
660
props = self._DetachFromSizer(sizer)
661
sp.ScrolledPanel.SetSizer(self, sizer)
662
self._AddToNewSizer(sizer, props)
665
class SizedDialog(wx.Dialog):
666
def __init__(self, *args, **kwargs):
669
Controls added to its content pane will automatically be added to
673
'self' is a SizedDialog instance
675
pane = self.GetContentsPane()
676
pane.SetSizerType("horizontal")
678
b1 = wx.Button(pane, wx.ID_ANY)
679
t1 = wx.TextCtrl(pane, wx.ID_ANY)
680
t1.SetSizerProps(expand=True)
683
wx.Dialog.__init__(self, *args, **kwargs)
685
self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY)
688
self.mainPanel = SizedPanel(self, -1)
690
mysizer = wx.BoxSizer(wx.VERTICAL)
691
mysizer.Add(self.mainPanel, 1, wx.EXPAND | wx.ALL, self.GetDialogBorder())
692
self.SetSizer(mysizer)
694
self.SetAutoLayout(True)
696
def GetContentsPane(self):
698
Return the pane to add controls too
700
return self.mainPanel
702
def SetButtonSizer(self, sizer):
703
self.GetSizer().Add(sizer, 0, wx.EXPAND | wx.BOTTOM | wx.RIGHT, self.GetDialogBorder())
705
# Temporary hack to fix button ordering problems.
706
cancel = self.FindWindowById(wx.ID_CANCEL, parent=self)
707
no = self.FindWindowById(wx.ID_NO, parent=self)
709
cancel.MoveAfterInTabOrder(no)
711
class SizedFrame(wx.Frame):
712
def __init__(self, *args, **kwargs):
716
Controls added to its content pane will automatically be added to
720
'self' is a SizedFrame instance
722
pane = self.GetContentsPane()
723
pane.SetSizerType("horizontal")
725
b1 = wx.Button(pane, wx.ID_ANY)
726
t1 = wx.TextCtrl(pane, -1)
727
t1.SetSizerProps(expand=True)
729
wx.Frame.__init__(self, *args, **kwargs)
732
# this probably isn't needed, but I thought it would help to make it consistent
733
# with SizedDialog, and creating a panel to hold things is often good practice.
734
self.mainPanel = SizedPanel(self, -1)
736
mysizer = wx.BoxSizer(wx.VERTICAL)
737
mysizer.Add(self.mainPanel, 1, wx.EXPAND)
738
self.SetSizer(mysizer)
740
self.SetAutoLayout(True)
742
def GetContentsPane(self):
744
Return the pane to add controls too
746
return self.mainPanel