在 wxPython 有两种方法对窗口内的 widgets 进行布局:
绝对定位(Absolute Positioning)
首先必须了解一下因使用绝对定位而造成的问题:
下面的例子是一个简单编辑器的框架,使用了绝对定位布局,可以发现,当我们改变窗口大小, wx.TextCtrl 的大小并没有随之改变。
import wx
class Absolute(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(250, 180))
panel = wx.Panel(self, -1)
menubar = wx.MenuBar()
file = wx.Menu()
edit = wx.Menu()
help = wx.Menu()
menubar.Append(file, ‘&File’)
menubar.Append(edit, ‘&Edit’)
menubar.Append(help, ‘&Help’)
self.SetMenuBar(menubar)
wx.TextCtrl(panel, -1, pos=(-1, -1), size=(250, 150))
self.Centre()
self.Show(True)
app = wx.App(0)
Absolute(None, -1, ”)
app.MainLoop()
在 wx.TextCtrl 的构造函数里使用了绝对定位的参数。
使用 sizers
sizers 可以解决上面使用绝对定位而造成的布局问题。下面是一些可选反对使用的 sizers:
import wx
class Sizer(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(250, 180))
menubar = wx.MenuBar()
file = wx.Menu()
edit = wx.Menu()
help = wx.Menu()
menubar.Append(file, ‘&File’)
menubar.Append(edit, ‘&Edit’)
menubar.Append(help, ‘&Help’)
self.SetMenuBar(menubar)
wx.TextCtrl(self, -1)
self.Centre()
self.Show(True)
app = wx.App(0)
Sizer(None, -1, ”)
app.MainLoop()
你可能会问,在上面的代码中怎么没见到任何的 sizers 对象?实际上,wx.Frame 已经内建了一个 sizer,而且,只能放置一个 widget 到 wx.Frame 上。
wx.BoxSizer
可以使放置其上的 widgets 成行或者成列排列,我们也可以放置 sizer 到另一个 sizer 上,以便于创建复杂的布局。
参数 orientation(方向)可以是 wx.VERTICAL 或者 wx.HORIZONTAL。通过 Add() 方法可以添加 widgets 到 sizer 上。proportion(比例)参数。假设我们将三个按钮添加到一个 horizontal wx.BoxSizer 上,它们的 proportions 参数分别为 0、1、2。当窗口发生改变时, proportion 参数被设置为0的按钮不会在水平方向发生改变,而被设置为2 的按钮的大小始终是设置为1的按钮的两倍。border 参数用于控制边框,可以下面的值或者它们的组合值:
import wx
class Border(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(250, 200))
panel = wx.Panel(self, -1)
panel.SetBackgroundColour(‘#4f5049′)
vbox = wx.BoxSizer(wx.VERTICAL)
midPan = wx.Panel(panel, -1)
midPan.SetBackgroundColour(‘#ededed’)
vbox.Add(midPan, 1, wx.EXPAND | wx.ALL, 20)
panel.SetSizer(vbox)
self.Centre()
self.Show(True)
app = wx.App()
Border(None, -1, ”)
app.MainLoop()
上面的代码将为 midPan 面板的四边添加宽度为20像素的边框。
wx.EXPAND 使 widget 占据分配到的全部空间。最后,我们也可以定义 widgets 的对齐方式:
实例(Go To Class)
在下面的例子中,我们将引入几个重要的构思。
import wx
class GoToClass(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(390, 350))
panel = wx.Panel(self, -1)
font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
font.SetPointSize(9)
vbox = wx.BoxSizer(wx.VERTICAL)
hbox1 = wx.BoxSizer(wx.HORIZONTAL)
st1 = wx.StaticText(panel, -1, ‘Class Name’)
st1.SetFont(font)
hbox1.Add(st1, 0, wx.RIGHT, 8)
tc = wx.TextCtrl(panel, -1)
hbox1.Add(tc, 1)
vbox.Add(hbox1, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 10)
vbox.Add((-1, 10))
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
st2 = wx.StaticText(panel, -1, ‘Matching Classes’)
st2.SetFont(font)
hbox2.Add(st2, 0)
vbox.Add(hbox2, 0, wx.LEFT | wx.TOP, 10)
vbox.Add((-1, 10))
hbox3 = wx.BoxSizer(wx.HORIZONTAL)
tc2 = wx.TextCtrl(panel, -1, style=wx.TE_MULTILINE)
hbox3.Add(tc2, 1, wx.EXPAND)
vbox.Add(hbox3, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)
vbox.Add((-1, 25))
hbox4 = wx.BoxSizer(wx.HORIZONTAL)
cb1 = wx.CheckBox(panel, -1, ‘Case Sensitive’)
cb1.SetFont(font)
hbox4.Add(cb1)
cb2 = wx.CheckBox(panel, -1, ‘Nested Classes’)
cb2.SetFont(font)
hbox4.Add(cb2, 0, wx.LEFT, 10)
cb3 = wx.CheckBox(panel, -1, ‘Non-Project classes’)
cb3.SetFont(font)
hbox4.Add(cb3, 0, wx.LEFT, 10)
vbox.Add(hbox4, 0, wx.LEFT, 10)
vbox.Add((-1, 25))
hbox5 = wx.BoxSizer(wx.HORIZONTAL)
btn1 = wx.Button(panel, -1, ‘Ok’, size=(70, 30))
hbox5.Add(btn1, 0)
btn2 = wx.Button(panel, -1, ‘Close’, size=(70, 30))
hbox5.Add(btn2, 0, wx.LEFT | wx.BOTTOM , 5)
vbox.Add(hbox5, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10)
panel.SetSizer(vbox)
self.Centre()
self.Show(True)
app = wx.App()
GoToClass(None, -1, ‘Go To Class’)
app.MainLoop()
我们创建一个 vertical sizer,并且将5个 horizontal sizers 放置于上面。
设置窗口字体的大小为9px。
实例(Find/Replace Dialog)
下面是一个复杂的例子 ── 查找/替换对话框。
import wx
class FindReplace(wx.Dialog):
def __init__(self, parent, id, title):
wx.Dialog.__init__(self, parent, id, title, size=(255, 365))
vbox_top = wx.BoxSizer(wx.VERTICAL)
panel = wx.Panel(self, -1)
vbox = wx.BoxSizer(wx.VERTICAL)
# panel1
panel1 = wx.Panel(panel, -1)
grid1 = wx.GridSizer(2, 2)
grid1.Add(wx.StaticText(panel1, -1, ‘Find: ‘, (5, 5)), 0, wx.ALIGN_CENTER_VERTICAL)
grid1.Add(wx.ComboBox(panel1, -1, size=(120, -1)))
grid1.Add(wx.StaticText(panel1, -1, ‘Replace with: ‘, (5, 5)), 0, wx.ALIGN_CENTER_VERTICAL)
grid1.Add(wx.ComboBox(panel1, -1, size=(120, -1)))
panel1.SetSizer(grid1)
vbox.Add(panel1, 0, wx.BOTTOM | wx.TOP, 9)
# panel2
panel2 = wx.Panel(panel, -1)
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
sizer21 = wx.StaticBoxSizer(wx.StaticBox(panel2, -1, ‘Direction’), orient=wx.VERTICAL)
sizer21.Add(wx.RadioButton(panel2, -1, ‘Forward’, style=wx.RB_GROUP))
sizer21.Add(wx.RadioButton(panel2, -1, ‘Backward’))
hbox2.Add(sizer21, 1, wx.RIGHT, 5)
sizer22 = wx.StaticBoxSizer(wx.StaticBox(panel2, -1, ‘Scope’), orient=wx.VERTICAL)
# we must define wx.RB_GROUP style, otherwise all 4 RadioButtons would be mutually exclusive
sizer22.Add(wx.RadioButton(panel2, -1, ‘All’, style=wx.RB_GROUP))
sizer22.Add(wx.RadioButton(panel2, -1, ‘Selected Lines’))
hbox2.Add(sizer22, 1)
panel2.SetSizer(hbox2)
vbox.Add(panel2, 0, wx.BOTTOM, 9)
# panel3
panel3 = wx.Panel(panel, -1)
sizer3 = wx.StaticBoxSizer(wx.StaticBox(panel3, -1, ‘Options’), orient=wx.VERTICAL)
vbox3 = wx.BoxSizer(wx.VERTICAL)
grid = wx.GridSizer(3, 2, 0, 5)
grid.Add(wx.CheckBox(panel3, -1, ‘Case Sensitive’))
grid.Add(wx.CheckBox(panel3, -1, ‘Wrap Search’))
grid.Add(wx.CheckBox(panel3, -1, ‘Whole Word’))
grid.Add(wx.CheckBox(panel3, -1, ‘Incremental’))
vbox3.Add(grid)
vbox3.Add(wx.CheckBox(panel3, -1, ‘Regular expressions’))
sizer3.Add(vbox3, 0, wx.TOP, 4)
panel3.SetSizer(sizer3)
vbox.Add(panel3, 0, wx.BOTTOM, 15)
# panel4
panel4 = wx.Panel(panel, -1)
sizer4 = wx.GridSizer(2, 2, 2, 2)
sizer4.Add(wx.Button(panel4, -1, ‘Find’, size=(120, -1)))
sizer4.Add(wx.Button(panel4, -1, ‘Replace/Find’, size=(120, -1)))
sizer4.Add(wx.Button(panel4, -1, ‘Replace’, size=(120, -1)))
sizer4.Add(wx.Button(panel4, -1, ‘Replace All’, size=(120, -1)))
panel4.SetSizer(sizer4)
vbox.Add(panel4, 0, wx.BOTTOM, 9)
# panel5
panel5 = wx.Panel(panel, -1)
sizer5 = wx.BoxSizer(wx.HORIZONTAL)
sizer5.Add((191, -1), 1, wx.EXPAND | wx.ALIGN_RIGHT)
sizer5.Add(wx.Button(panel5, -1, ‘Close’, size=(50, -1)))
panel5.SetSizer(sizer5)
vbox.Add(panel5, 1, wx.BOTTOM, 9)
vbox_top.Add(vbox, 1, wx.LEFT, 5)
panel.SetSizer(vbox_top)
self.Centre()
self.ShowModal()
self.Destroy()
app = wx.App()
FindReplace(None, -1, ‘Find/Replace’)
app.MainLoop()
备注:对于 Windows 用户,请在 ShowModal() 行前加入 self.SetClientSize(panel.GetBestSize()) 行。
wx.GridSizer
这是 wx.GridSizer 的构造函数。分别定义布局表格的行列数以及行列的大小。
下面使用 wx.GridSizer 来构造一个计算器的基本骨架。
import wx
class GridSizer(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(300, 250))
menubar = wx.MenuBar()
file = wx.Menu()
file.Append(1, ‘&Quit’, ‘Exit Calculator’)
menubar.Append(file, ‘&File’)
self.SetMenuBar(menubar)
self.Bind(wx.EVT_MENU, self.OnClose, id=1)
sizer = wx.BoxSizer(wx.VERTICAL)
self.display = wx.TextCtrl(self, -1, ”, style=wx.TE_RIGHT)
sizer.Add(self.display, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 4)
gs = wx.GridSizer(4, 4, 3, 3)
gs.AddMany( [(wx.Button(self, -1, 'Cls'), 0, wx.EXPAND),
(wx.Button(self, -1, 'Bck'), 0, wx.EXPAND),
(wx.StaticText(self, -1, ''), 0, wx.EXPAND),
(wx.Button(self, -1, 'Close'), 0, wx.EXPAND),
(wx.Button(self, -1, '7'), 0, wx.EXPAND),
(wx.Button(self, -1, '8'), 0, wx.EXPAND),
(wx.Button(self, -1, '9'), 0, wx.EXPAND),
(wx.Button(self, -1, '/'), 0, wx.EXPAND),
(wx.Button(self, -1, '4'), 0, wx.EXPAND),
(wx.Button(self, -1, '5'), 0, wx.EXPAND),
(wx.Button(self, -1, '6'), 0, wx.EXPAND),
(wx.Button(self, -1, '*'), 0, wx.EXPAND),
(wx.Button(self, -1, '1'), 0, wx.EXPAND),
(wx.Button(self, -1, '2'), 0, wx.EXPAND),
(wx.Button(self, -1, '3'), 0, wx.EXPAND),
(wx.Button(self, -1, '-'), 0, wx.EXPAND),
(wx.Button(self, -1, '0'), 0, wx.EXPAND),
(wx.Button(self, -1, '.'), 0, wx.EXPAND),
(wx.Button(self, -1, '='), 0, wx.EXPAND),
(wx.Button(self, -1, '+'), 0, wx.EXPAND) ])
sizer.Add(gs, 1, wx.EXPAND)
self.SetSizer(sizer)
self.Centre()
self.Show(True)
def OnClose(self, event):
self.Close()
app = wx.App()
GridSizer(None, -1, ‘GridSizer’)
app.MainLoop()
注意,我们在 Bck 和 Close 按钮之间放置一个空白的 wx.StaticText,这是布局上的一个有用技巧。
例子中的 AddMany() 方法可以一次过放置多个 widgets 到 sizer 上。
widgets 放置在布局表格上是按一定顺序的,先放满第一行,再放置第二行…如此类推。
wx.FlexGridSizer
wx.FlexGridSizer 跟 wx.GridSizer 相似,但提供更多的灵活性。在 wx.GridSizer 中,所有的单元格都必须大小相同。而在 wx.FlexGridSizer 中,同行的单元格的高度相同,同列的单元格的宽度相同,而不同的列和行可以高宽度不同。
这是 wx.FlesGridSizer 的构造函数,跟 wx.GridSizer 的构造函数相似。
import wx
class FlexGridSizer(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(290, 250))
panel = wx.Panel(self, -1)
hbox = wx.BoxSizer(wx.HORIZONTAL)
fgs = wx.FlexGridSizer(3, 2, 9, 25)
title = wx.StaticText(panel, -1, ‘Title’)
author = wx.StaticText(panel, -1, ‘Author’)
review = wx.StaticText(panel, -1, ‘Review’)
tc1 = wx.TextCtrl(panel, -1)
tc2 = wx.TextCtrl(panel, -1)
tc3 = wx.TextCtrl(panel, -1, style=wx.TE_MULTILINE)
fgs.AddMany([(title), (tc1, 1, wx.EXPAND), (author), (tc2, 1, wx.EXPAND),
(review, 1, wx.EXPAND), (tc3, 1, wx.EXPAND)])
fgs.AddGrowableRow(2, 1)
fgs.AddGrowableCol(1, 1)
hbox.Add(fgs, 1, wx.ALL | wx.EXPAND, 15)
panel.SetSizer(hbox)
self.Centre()
self.Show(True)
app = wx.App()
FlexGridSizer(None, -1, ‘FlexGridSizer’)
app.MainLoop()
我们设置第三行和第二列为可变大小,这样,当窗口大小改变时,文本控件也随之改变。当然,不要忘记设置相应的 widgets 的 wx.EXPAND。
wx.GridBagSizer
这是 wxPython 中最复杂的一个 sizer,它可以精确的定位 widgets。下面是 wx.GridBagSizer 的构造函数:
通过 Add() 方法放置 widgets:
pos 参数指定放置于虚拟表格的位置 ── 左上角为 (0,0)。span 参数是一个可选参数,指定 widgets 的跨度,例如,(3,2) 表示 widget 跨越 3 行 2 列。falg 和 border 参数跟 wx.BoxSizer 的相同。如果想设置可改变的表格,可以使用下面的方法:
实例(Rename dialog)
这是一个简单的示例。我们不必因为 wx.GridBagSizer 的过于复杂而担心,一旦我们理解它的原理,使用起来也会很简单的。
import wx
class Rename(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(320, 130))
panel = wx.Panel(self, -1)
sizer = wx.GridBagSizer(4, 4)
text = wx.StaticText(panel, -1, ‘Rename To’)
sizer.Add(text, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)
tc = wx.TextCtrl(panel, -1)
sizer.Add(tc, (1, 0), (1, 5), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
buttonOk = wx.Button(panel, -1, ‘Ok’, size=(90, 28))
buttonClose = wx.Button(panel, -1, ‘Close’, size=(90, 28))
sizer.Add(buttonOk, (3, 3))
sizer.Add(buttonClose, (3, 4), flag=wx.RIGHT | wx.BOTTOM, border=5)
sizer.AddGrowableCol(1)
sizer.AddGrowableRow(2)
panel.SetSizerAndFit(sizer)
self.Centre()
self.Show(True)
app = wx.App()
Rename(None, -1, ‘Rename Dialog’)
app.MainLoop()
将文本 “Rename to”放于窗口的左上角,因此将 pos 指定为 (0,0)。
将文本输入框放于第二行的始端(1,0) ── 紧记,是用 0 开始计数的。同时文本框跨越1行5列(1,5)。
在第4行放置两个按钮(第三行我们空着),分别放于第4列和第5列。
实例(Open Resource)
import wx
class OpenResource(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(400, 500))
panel = wx.Panel(self, -1)
sizer = wx.GridBagSizer(4, 4)
text1 = wx.StaticText(panel, -1, ‘Select a resource to open’)
sizer.Add(text1, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)
tc = wx.TextCtrl(panel, -1)
sizer.Add(tc, (1, 0), (1, 3), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
text2 = wx.StaticText(panel, -1, ‘Matching resources’)
sizer.Add(text2, (2, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)
list1 = wx.ListBox(panel, -1, style=wx.LB_ALWAYS_SB)
sizer.Add(list1, (3, 0), (5, 3), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
text3 = wx.StaticText(panel, -1, ‘In Folders’)
sizer.Add(text3, (8, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)
list2 = wx.ListBox(panel, -1, style=wx.LB_ALWAYS_SB)
sizer.Add(list2, (9, 0), (3, 3), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
cb = wx.CheckBox(panel, -1, ‘Show derived resources’)
sizer.Add(cb, (12, 0), flag=wx.LEFT | wx.RIGHT, border=5)
buttonOk = wx.Button(panel, -1, ‘OK’, size=(90, 28))
buttonCancel = wx.Button(panel, -1, ‘Cancel’, size=(90, 28))
sizer.Add(buttonOk, (14, 1))
sizer.Add(buttonCancel, (14, 2), flag=wx.RIGHT | wx.BOTTOM, border=5)
help = wx.BitmapButton(panel, -1, wx.Bitmap(‘icons/help16.png’), style=wx.NO_BORDER)
sizer.Add(help, (14, 0), flag=wx.LEFT, border=5)
sizer.AddGrowableCol(0)
sizer.AddGrowableRow(3)
sizer.AddGrowableRow(9)
sizer.SetEmptyCellSize((5, 5))
panel.SetSizer(sizer)
self.Centre()
self.Show(True)
app = wx.App()
OpenResource(None, -1, ‘Open Resource’)
app.MainLoop()
实例(Create new class)
import wx
class NewClass(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)
panel = wx.Panel(self, -1)
sizer = wx.GridBagSizer(0, 0)
text1 = wx.StaticText(panel, -1, ‘Java Class’)
sizer.Add(text1, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15)
icon = wx.StaticBitmap(panel, -1, wx.Bitmap(‘icons/exec.png’))
sizer.Add(icon, (0, 4), flag=wx.LEFT, border=45)
line = wx.StaticLine(panel, -1 )
sizer.Add(line, (1, 0), (1, 5), wx.TOP | wx.EXPAND, -15)
text2 = wx.StaticText(panel, -1, ‘Name’)
sizer.Add(text2, (2, 0), flag=wx.LEFT, border=10)
tc1 = wx.TextCtrl(panel, -1, size=(-1, 30))
sizer.Add(tc1, (2, 1), (1, 3), wx.TOP | wx.EXPAND, -5)
text3 = wx.StaticText(panel, -1, ‘Package’)
sizer.Add(text3, (3, 0), flag= wx.LEFT | wx.TOP, border=10)
tc2 = wx.TextCtrl(panel, -1)
sizer.Add(tc2, (3, 1), (1, 3), wx.TOP | wx.EXPAND, 5)
button1 = wx.Button(panel, -1, ‘Browse…’, size=(-1, 30))
sizer.Add(button1, (3, 4), (1, 1), wx.TOP | wx.LEFT | wx.RIGHT , 5)
text4 = wx.StaticText(panel, -1, ‘Extends’)
sizer.Add(text4, (4, 0), flag=wx.TOP | wx.LEFT, border=10)
combo = wx.ComboBox(panel, -1, )
sizer.Add(combo, (4, 1), (1, 3), wx.TOP | wx.EXPAND, 5)
button2 = wx.Button(panel, -1, ‘Browse…’, size=(-1, 30))
sizer.Add(button2, (4, 4), (1, 1), wx.TOP | wx.LEFT | wx.RIGHT , 5)
sb = wx.StaticBox(panel, -1, ‘Optional Attributes’)
boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL)
boxsizer.Add(wx.CheckBox(panel, -1, ‘Public’), 0, wx.LEFT | wx.TOP, 5)
boxsizer.Add(wx.CheckBox(panel, -1, ‘Generate Default Constructor’), 0, wx.LEFT, 5)
boxsizer.Add(wx.CheckBox(panel, -1, ‘Generate Main Method’), 0, wx.LEFT | wx.BOTTOM, 5)
sizer.Add(boxsizer, (5, 0), (1, 5), wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT , 10)
button3 = wx.Button(panel, -1, ‘Help’, size=(-1, 30))
sizer.Add(button3, (7, 0), (1, 1), wx.LEFT, 10)
button4 = wx.Button(panel, -1, ‘Ok’, size=(-1, 30))
sizer.Add(button4, (7, 3), (1, 1), wx.LEFT, 10)
button5 = wx.Button(panel, -1, ‘Cancel’, size=(-1, 30))
sizer.Add(button5, (7, 4), (1, 1), wx.LEFT | wx.BOTTOM | wx.RIGHT, 10)
sizer.AddGrowableCol(2)
sizer.Fit(self)
panel.SetSizer(sizer)
self.Centre()
self.Show(True)
app = wx.App()
NewClass(None, -1, ‘Create Java Class’)
app.MainLoop()
注意,这里我们使用负数来设置 top border,这相当于设置 bottom border 为15px。
确保窗口的大小可以覆盖所有 widgets。