使用wxPython打造自己的印象笔记:布局

438 阅读10分钟

这篇文章将介绍一下wxPython的基础知识,重点是学会如何布局,这样就能够快速上手开发了。

首先保证Python已经安装好,版本最好是3.7或者以上,然后安装wxPython这个模块。

pip install wxPython

安装好了之后,写一个hello world。

import wx
app = wx.App()
wx.Frame(None, title="Hello World").Show()
app.MainLoop()

运行如图所示,显示了一个空白窗体,标题是Hello World。


接下来讲解一下代码,通过 import wx引入wxPython模块,然后通过wx.App()来初始化GUI应用。

wx.App

在wxPython的GUI应用中, wx.App是必不可少的部分,只有将其初始化之后才可以初始化其他的组件,例如这里的wx.Framewx.App通常的作用有三点:

  1. 初始化GUI应用和应用中的其他组件
  2. 读取和设置应用的属性
  3. 实现窗体的事件循环

wx.Frame

wx.Frame是一种常见的窗体,frame可以有标题栏,状态栏,工具栏以及菜单栏。初始化之后,调用Show方法来显示frame。

事件循环

事件是GUI应用中必需的一部分,事件就是一种应用层面的信息,例如鼠标点击就是一个事件。wxPython通过 MainLoop方法来监听并处理事件直到整个应用退出。

常用组件

常见的组件有按钮、文本框、窗体、下拉框等等,我简单的做了一个图示。


其他的常用组件大家可以去组件展示页面看看。

好了,接下来了解一下常用的布局方式吧,这也是这篇文章的重点。

通常一个GUI应用包含很多的组件,这些组件会放置在父容器里面。如何设置组件的位置是很有必要的,在wxPython中可以使用绝对定位和布局器定位。

绝对定位

通过指定组件的大小和位置来实现定位,先来看个例子。

import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='绝对定位')
        self.st_name = wx.StaticText(self, label='用户名', pos=(0,0))
        self.tc_name = wx.TextCtrl(self, pos=(80, 0), size=(100,-1))

app = wx.App()
MyFrame().Show()
app.MainLoop()

pos参数表示x和y轴坐标,坐标轴原点位于父组件最左上方。size参数表示长度和高度, -1表示系统默认值,这里我们将输入框的大小设置为(100,-1),表示长度为100像素,高度为默认值。运行之后如图所示。


改变pos参数可以调整文本和输入框的位置,动手试一试!

除了在组件初始化时指定pos参数来定位,还可以通过wx.Window.SetPosition方法 指定位置。上面的代码可以做如下修改。

self.tc_name = wx.TextCtrl(self)
self.tc_name.SetPosition((80, 0))

绝对定位因为把位置和大小限定死了,所以有几点不足:

  1. 大小和位置一旦确定之后就固定了,不会随着窗体大小改变而变化。
  2. 在不同的平台上,组件的效果不一致。
  3. 如果需要调整布局,还需要重新指定位置和大小。

wx.Sizer

前面提到了绝对定位的几个缺点,使用布局器可以避免这些不足(sizer我就翻译成布局器了)。布局器实际上就是wx.Sizer的几个子类,有如下几类布局器:

  • wx.BoxSizer
  • wx.StaticBoxSizer
  • wx.GridSizer
  • wx.FlexGridSizer
  • wx.GridBagSizer

wx.BoxSizer

BoxSizer可以将组件一起排列在同一列或者同一行,例如可以使用BoxSizer将三个按钮排成一行,如下所示。

import wx

class Frame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='BoxSizer示例')
        self.btn_1 = wx.Button(self, label='按钮1')
        self.btn_2 = wx.Button(self, label='按钮2')
        self.btn_3 = wx.Button(self, label='按钮3')

        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        main_sizer.Add(self.btn_1)
        main_sizer.Add(self.btn_2)
        main_sizer.AddSpacer(50)
        main_sizer.Add(self.btn_3)
        self.SetSizer(main_sizer)

app = wx.App()
Frame().Show()
app.MainLoop()

运行之后如图所示。


如果我们需要将这三个按钮排成一列,只需要在初始化布局器时设置wx.VERTICAL即可。将相应的代码修改为main_sizer = wx.BoxSizer(wx.VERTICAL)重新运行,可以看到三个按钮已经排成一列了。

空间分配

上面的例子中,三个按钮排成一行时,每个按钮的尺寸为默认大小,如果我们希望第一个按钮占据这一行的剩余空间, 后面的两个按钮的尺寸保持为默认值,就需要用到proportion来指定空间占比了。proportion默认值为0,如 果我们希望第一个按钮能够占据剩余空间,只需要将它的比例设置为1即可。

将对应的代码修改为main_sizer.Add(self.btn_1, proportion=1),再运行程序会看到第一个按钮已经占据了 剩余空间。

proportion参数用来指定排列方向上面的比例,我们前面的例子排列方向为wx.HORIZONTAL,也就是横向排列, 所以对于剩余空间的分配会在横向进行。如果需要在交叉方向上,也就是纵向上占据剩余空间,则需要设置wx.EXPAND标记。

例如我们将第一个按钮设置为填充整个纵向空间,则将对应的代码修改为 main_sizer.Add(self.btn_1, flag=wx.EXPAND)。 此时按钮会占据其所在的纵向区域。

对齐方向

默认情况下,组件是按照从左往右,从上到下的次序对齐的,如果我们需要指定布局器里面的某个组件的对齐方式,就需要 设置wx.ALIGN_*标记,例如wx.ALIGN_TOP表示顶部对齐,wx.ALIGN_RIGHT表示向右对齐等等。但是需要注意的 左右对齐标志只适用于垂直布局,而上下对齐标志只适用于水平布局。

如果我们需要将第三个按钮向右对齐,因为第三个按钮是属于水平布局器里面的一个组件,所以直接添加wx.ALIGN_RIGHT 是没有效果的,我们需要添加一个空白组件将第三个按钮挤压到最右边。使用AddStretchSpacer方法可以添加一个默认比例 为1的空白组件,上述代码修改为:

import wx

class Frame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='BoxSizer示例')
        self.btn_1 = wx.Button(self, label='按钮1')
        self.btn_2 = wx.Button(self, label='按钮2')
        self.btn_3 = wx.Button(self, label='按钮3')

        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        main_sizer.Add(self.btn_1)
        main_sizer.Add(self.btn_2)
        main_sizer.AddStretchSpacer()
        main_sizer.Add(self.btn_3)
        self.SetSizer(main_sizer)

app = wx.App()
Frame().Show()
app.MainLoop()

再次运行如图所示。

如果我们需要将第三个按钮和第二个按钮之间留出50像素的空白,除了在布局器里面设置border参数以外,还可以使用AddSpacer方法,将上面例子的main_sizer.AddStretchSpacer()修改为main_sizer.AddSpacer(50)即可。

wx.StaticBoxSizer

StaticBoxSizer和BoxSizer特别类似,只不过在布局器的周围添加了一个StaticBox,StaticBox是一个环绕着子组件的矩形框, 标题在矩形框上方。

依然以三个按钮为例,我们写一个简单的例子。

import wx

class Frame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='static box sizer')
        sizer = wx.StaticBoxSizer(wx.HORIZONTAL, self, label='选项')

        self.btn_1 = wx.Button(self, label='按钮1')
        self.btn_2 = wx.Button(self, label='按钮2')
        self.btn_3 = wx.Button(self, label='按钮3')

        sizer.Add(self.btn_1)
        sizer.Add(self.btn_2)
        sizer.Add(self.btn_3)
        self.SetSizer(sizer)

app = wx.App()
Frame().Show()
app.MainLoop()

可以看到,wx.StaticBoxSizerwx.BoxSizer多了两个初始化参数,第一个表示wx.StaticBox所关联的窗体,第二个表示标题。还有一种初始化方法定义为:__init__ (self, box, orient=HORIZONTAL),如果已有一个wx.StaticBox实 例,就可以使用这种方法来初始化。

wx.GridSizer

wx.GridSizer将组件分布在一个表格的单元格里面,每个单元格大小相同,宽度为所有组件的最大宽度,高度为所有组件的最 大高度。并且可以指定单元格之间的水平间隔和垂直间隔大小。

我们先来看一个简单的例子,把十个按钮排列成两行五列。

import wx

class Frame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Grid Sizer')
        sizer = wx.GridSizer(5)
        for i in range(10):
            sizer.Add(wx.Button(self, label=f"按钮{i+1}"))
        self.SetSizer(sizer)

app = wx.App()
Frame().Show()
app.MainLoop()

运行如图所示。

改变窗体大小,发现按钮依然是按照每行五列均匀排列,这是因为在初始化网格布局器的时候,我们指定了列数为5,网格布局器的构造函数有这么几个:

  • GridSizer(cols, vgap, hgap)
  • GridSizer(cols, gap=Size(0,0))
  • GridSizer(rows, cols, vgap, hgap)
  • GridSizer(rows, cols, gap)

上面的例子中我们使用了第二个构造函数,默认的间距为0。

wx.FlexGridSizer

wx.FlexGridSizer继承自wx.GridSizer。弹性网格布局器的同一行单元格的高度相同,同一列单元格的宽度相同,但所有的单元格不一定具有相同的大小。弹性网格布局器可以通过AddGrowableCol(self, idx, proportion=0)AddGrowableRow(self, idx, proportion=0)指定特定的行或者列的收缩比例,方便自适应布局,常用于表单界面中,接下来我们就来实现一个简单的表单窗体。

import wx

class Frame(wx.Frame):
    def __init__(self):
        super().__init__(None, title = 'flex grid sizer')
        main_sizer = wx.BoxSizer()

        sizer = wx.FlexGridSizer(2, (10,20))
        sizer.Add(wx.StaticText(self, label='昵称'))
        sizer.Add(wx.TextCtrl(self), flag=wx.EXPAND)
        sizer.Add(wx.StaticText(self, label='留言'))
        sizer.Add(wx.TextCtrl(self, style=wx.TE_MULTILINE), flag=wx.EXPAND, proportion=1)

        sizer.AddGrowableRow(1)
        sizer.AddGrowableCol(1)

        main_sizer.Add(sizer, flag=wx.EXPAND|wx.ALL, proportion=1, border=10)
        self.SetSizer(main_sizer)

app = wx.App()
Frame().Show()
app.MainLoop()

运行之后如图所示。

为了让控件四周有10像素的间距,我们将弹性网格布局器嵌入到一个盒子布局器里面。通过wx.FlexGridSizer(2, (10,20))构造 出一个两列,单元格水平和垂直间距分别为10和20的伸缩网格布局器,接着按照从左往右、从上到下的顺序添加所需控件,这里我们添加了标签和按钮控件。

接下来通过sizer.AddGrowableRow(1)sizer.AddGrowableCol(1)来指定第二行的伸缩比例 为1,指定第二列的伸缩比例为1,方法签名为:

  • AddGrowableRow(self, idx, proportion=0)
  • AddGrowableCol(self, idx, proportion=0)

因为默认的伸缩比例为0,所以比例为1的话就会占据剩余空间。行占据剩余空间意味着高度会变化,列占据剩余空间意味着宽度会变化。 从表单界面可以看到,昵称输入框和留言输入框的宽度都占据了剩余空间,这是通过sizer.AddGrowableCol(1)来实现的,并且通过sizer.AddGrowableRow(1)让第二行的高度占据了剩余空间。这里的proportion参数和布局器中的含义是相同的,都表示伸缩比例,所不同的是这里的比例如果都是0,则表明所有的行或者列是均匀伸缩的,而比例为0在布局器中则不会伸缩。

在改变行和列的分配比例后,要想让控件的大小发生变化,我们还需要在添加控件的时候设置高度和宽度如何伸缩,我们使用wx.EXPAND指定控件水平放大,通过proportion=1指定控件竖直放大。默认情况下,输入框wx.TextCtrl只能输入单行文本,通过style=wx.TE_MULTILINE指定输入框允许多行文本输入,这样就可以改变输入框的高度了。 最后让main_sizer添加弹性网格布局器,设置水平竖直同时放大,并添加10像素的边框。

wx.GridBagSizer

wx.GridBagSizer继承自wx.FlexGridSizer,它允许对每个单元格单独设置尺寸,而后者每一行中的单元格高度相同,每一列中的单元格宽度相同。我们先来看一个简单的例子。

import wx

class Frame(wx.Frame):

    def __init__(self):
        super().__init__(None, title='grid bag sizer')

        sizer = wx.GridBagSizer(4, 4)

        sizer.Add(wx.StaticText(self, label="请输入文字"), pos=(0, 0), flag=wx.TOP|wx.LEFT|wx.BOTTOM, border=5)
        sizer.Add(wx.TextCtrl(self), pos=(1, 0), span=(1, 5), flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=5)

        sizer.Add(wx.Button(self, label="确定"), pos=(3, 3))
        sizer.Add(wx.Button(self, label="取消"), pos=(3, 4), flag=wx.RIGHT|wx.BOTTOM, border=10)

        sizer.AddGrowableCol(0)
        sizer.AddGrowableRow(2)

        self.SetSizer(sizer)

app = wx.App()
Frame().Show()
app.MainLoop()

运行之后如图所示。

首先我们初始化一个wx.GridBagSizer实例,设置间距为4*4大小。然后添加“请输入文字”的标签,注意通过pos参数指定了位置为(0,0), 也就是最左上角。

接下来添加了一个文本框,同样的通过pos参数指定位置为(1,0),也就是第二行第一列,并通过span参数指定单元格的大小,这里 span=(1,5)表示占据一行五列的区域,为了让文本框大小自适应,添加了wx.EXPAND标记。

最后添加两个按钮,第一个确定按钮的坐标是(3,3),也就是第四行第四列,第二个取消按钮同理放置在了第四行第五列。为了让布局随着窗体 大小改变而改变,我们还使用了AddGrowableColAddGrowableRow方法,上一节的wx.FlexGridSizer中已经提及过,分别让第一列 的宽度占据所有剩余空间和第三行的高度占据剩余空间,这样文本框的宽度和按钮的位置就实现了自适应。

以上简单的介绍了wxPython的几种常用的布局方式,希望大家能动手实践一下,下一篇将介绍事件处理。