wxPython图形界面

前面的教程中,我们程序的用户交互界面都是命令行终端窗口。

程序的用户交互界面,英文称之为 UI (user interface)

当一个应用的 UI 比较复杂的时候,命令行方式就不便用户使用了,这时我们需要图形界面。

如果用Python语言开发跨平台的图形界面的程序,主要有3种选择:

  • Tkinter

  • wxPython

  • PyQt

从上手的难易程度、界面的美观 和 License 综合考虑,个人倾向于使用 wxPython。

wxPython 封装了 对 跨平台的 图形界面库 wxWidgets 的 Python语言接口。 使得我们很方便的就可以使用 功能强大的 wxWidgets。 而且界面都是各运行平台原生的图形界面。

下面我们就介绍如何使用它开发图形界面程序。


安装

从 wxPython 4.0 版本开始, 安装就和普通库安装方式一样了。

直接 运行: pip install wxPython

从简单的程序开始

我们来看下面这样的一个简单的使用wxPython的程序。

import wx

app = wx.App(False) 
frame = wx.Frame(None, wx.ID_ANY, "Hello World") 

frame.Show(True)   
app.MainLoop()

大家运行一下,会发现出现如下的程序界面

image

我们来看看这里面每句代码的含义。

app = wx.App(False)

每个使用wxPython的应用 需要 创建 一个 wx.App 类实例。

通常,我们就直接 使用 wx.App 类来创建一个 实例。更复杂的应用可以继承这个类,添加更多的属性和行为。

“False” 参数的意思是 “不要将 stdout 和 stderr 的内容重定向到某个窗口中”.


wx.Frame(None,wx.ID_ANY,"Hello")

wx.Frame 是一种 最上层的 窗口(window)类, 也称顶级窗口 (top-level window) 。 顶级窗口 可以没有从属的的父级窗口。

它有三个初始化参数 (Parent, Id, Title) 分别代表 (父窗口, 指定的I号, 窗口标题栏内容)。

我们这个例子里面: Parent 值为 None 表示没有父窗口。 Id 值为 wx.ID_ANY 表示让库帮我们挑选一个ID, Title 值为 “hello” 就是标题栏内容为Hello


frame.Show(True)

上面只是创建了Frame窗口对象, 这行代码才真正让 这个Frame 显示出来。


app.MainLoop()

最后这句话,非常重要。

基本上任何图形界面库,都需要程序不断的处理界面上所产生的 事件。 比如用户点击了某个元素,用户按下了某个按键等等。

这些事件由用户触发,操作系统产生 并 发送给当前的应用程序去处理。

所以这句代码 内部的实现就是一个死循环, 不断的接收 用户触发的 事件,并且调用相应的处理函数 去处理这些事件。

事件的处理函数通常是由我们实现的, 比如我们需要定义一个点击按钮的事件。

可以修改一下代码,如下

import wx

app = wx.App(False) 
frame = wx.Frame(None, wx.ID_ANY, "Hello World", size=(400,300)) 

# 点击按钮次数
clickNum = 0
# 定义一个函数处理点击按钮的事件
def clickButton(event):
    global clickNum
    clickNum  += 1
    # 输出信息到标准输出上
    print(f"点击{clickNum}次")

# 通常 Frame里面不直接放控件,
# 定义一个 面板 Panel,里面放其它控件
panel = wx.Panel(frame, wx.ID_ANY)

# 定义按钮, pos参数指定按钮在panel里面的坐标
# size参数指定按钮控件的宽度和高度,单位为像素
button = wx.Button(panel, wx.ID_ANY, "点击试试",
        pos=(0, 0), size=(100,40))

# 指定该按钮的点击事件的处理函数是 clickButton      
button.Bind( wx.EVT_BUTTON, clickButton)

frame.Show(True)    
app.MainLoop()

运行该代码,我们发现产生如下界面

image

点击几次按钮后,在其背后的终端窗口出现如下内容

image

不要惊讶哦, 缺省情况下print函数的输出是输出到终端窗口上的。


其中,函数clickButton 被下面的代码注册为 那个按钮点击后应该调用的方法 。

button.Bind( wx.EVT_BUTTON, clickButton)

所以当程序运行的时候, 如果用户点击那个按钮, 操作系统就会将该点击事件发送给我们的程序,我们的程序就会调用这个注册的 处理那个按钮的函数。


clickButton 是全局函数, 并且里面使用了另外的全局变量。 这种方法可行,但是代码的可读性不太好。

通常我们可以定义一个继承自Frame的子类, 在类里面封装这些逻辑。 如下所示

import wx

class MyFrame(wx.Frame):
    """ 创建一个类继承自 Frame. """
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(400,300))

        # 通常 Frame里面不直接放控件,
        # 定义一个 面板 Panel,里面放其它控件
        self.panel = wx.Panel(self, wx.ID_ANY)


        # 定义按钮, pos参数指定按钮在panel里面的坐标
        # size参数指定按钮控件的宽度和高度,单位为像素
        self.button = wx.Button(
            self.panel, wx.ID_ANY, "点击试试",
            pos=(0, 0), size=(100,40))

        # 指定该按钮的点击事件的处理方法是 clickButton      
        self.button.Bind( wx.EVT_BUTTON, self.clickButton)
 
        self.clickNum = 0 # 点击按钮次数
        self.Show(True)
    
    def clickButton(self,event):
        self.clickNum += 1
        # 输出信息到标准输出上
        print(f"点击{self.clickNum}次")

app = wx.App(False)
frame = MyFrame(None, '图形界面测试应用')
app.MainLoop()

图形界面的布局

当界面上的元素(包括按钮、文本框、文本信息),比较多的时候,就需要我们设计界面的布局。

布局分为两种方式

  • 绝对坐标布局

  • Sizer 管理布局


绝对坐标布局,就是每个界面的元素都是自动其在父窗口的坐标,比如上面的代码

self.button = wx.Button(
            self.panel, wx.ID_ANY, "点击试试",
            pos=(0, 0), size=(100,40))

pos 参数 就是指定了Button这个元素 在其父窗口 ( 就是Panel) 的相对坐标。 这里的值是 (0, 0) 就是说,要放在panel的左上角。所以运行起来,这个按钮就是在左上角紧贴着。

这边的坐标是以显示器像素点 (pixel) 为单位的。

如果我们想改变其位置就改变pos参数,比如,修改为

self.button = wx.Button(
            self.panel, wx.ID_ANY, "点击试试",
            pos=(30, 30), size=(100,40))

运行一下,就发现按钮的位置改变了。


通常我们不采用决定坐标布局,因为它有如下的问题:

我们拖拽窗口边框 改变窗口大小的时候,控件之间的相对位置不会跟着改变,就会变的非常丑。

比如说,这样的一个程序

import wx

class MyFrame(wx.Frame):

    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(400,300))
        self.panel = wx.Panel(self, wx.ID_ANY)


        self.button1 = wx.Button(
            self.panel, wx.ID_ANY, "点击1",
            pos=(30, 30), size=(50,40))

        self.textCtrl1 = wx.TextCtrl( 
            self.panel, wx.ID_ANY, 
            pos=(30, 80), size=(280,130) )
 

        self.Show(True)

app = wx.App(False)
frame = MyFrame(None, '图形界面测试应用')
app.MainLoop()

大家可以运行一下, 并用鼠标拖拽窗口边框看看,就发现两个控件不会随着父窗口的变化而变化。


使用Sizer 和界面生成工具

所以我们通常会使用Sizer来管理布局。

Sizer 是wxpython里面的一个看不见的界面元素。 可以根据一定的规则 管理其内部的控件的显示位置。 并且在父窗口大小改变是,动态改变其内部控件的大小和距离。

即使使用Sizer,我们手工编写界面代码,还是显得比较麻烦。

所以白月黑羽推荐大家使用 界面生成软件,帮我们快速开发界面。

大家可以使用一款开源工具 wxFormBuilder , 在github上的网址是 https://github.com/wxFormBuilder/wxFormBuilder

可以点击这里,下载最新版本

image

下载到本地后,解压,运行里面的可执行程序 wxFormBuilder.exe 即可。

初次运行的界面如下

image

这个界面最左边的是界面元素层级关系的树形展示, 中间是所见即所得的界面编辑, 右边是当前选中元素的属性编辑框

要生成类似上面程序的界面,我们只需要如下操作。

  1. 点击 Forms 标签,随后点击 下方左边的 Frame 图标,就会创建一个Frame。如下所示

    image

  2. 点击 Layout 标签,随后点击 下方左边的 wxBoxSizer图标,就会在刚才产生的Frame里面创建一个wxBoxSizer。如下所示

    image

  3. 点击 Common 标签,随后点击 下方左边的 wxButton 图标,就会在刚才产生的wxBoxSizer里面创建一个 wxButton。

    并且,在右边的元素的属性对话框,把label 属性的值改为:点击1 , 这样按钮上的文字就是 点击1 , 如下所示

    image

  4. 点击 下方左边的 wxTextCtrl 图标,就会在刚才产生的wxBoxSizer里面创建一个 wxTextCtrl 文本编辑框。

    随后,请把右边的元素的属性对话框拉动到最下方,点击 flag 展开其属性设置,如下所示

    image

    在展开的内容中勾选 wxEXPAND 和 wxALL ,如下所示 image

     wxEXPAND 是元素的显示属性, 表示该元素宽度和高度将尽可能的伸展到规定的大小。
    
  5. Sizer可以定义其内部元素的显示比例。

    比如这里,我们的sizer是垂直sizer 。 点击坐标控件树里面的sizer ,可以看到其属性如下

    image

    wxVertical就是垂直的意思,表示这个sizer 是垂直分割界面的,里面的元素都是垂直存放的。

    如果你希望元素水平摆放,可以点击下拉框,改为 wxHORIZONTAL 。

    如果我们希望文本编辑框 和 按钮 所占区域的 高度大概是 3:1 的话,就可以分布修改他们的这个proportion这个属性,

    其值分别为 3 和 1 ,如下所示

    image

    image

    最终得到这样的界面

    image

  6. 界面大体已经生成, 我们可以点击这里,看到这样的界面对应的Python代码,如下所示

    image

    里面定义了MyFrame1类和其中的元素。 我们可以将代码拷贝到我们的代码文件中。

    最后添加一段代码:创建wxApp,并显示这个Frame,进入消息循环。

    就可以把这样的界面的程序运行起来了。 如下所示

     import wx
     import wx.xrc
    
     ###########################################################################
     ## Class MyFrame1
     ###########################################################################
    
     class MyFrame1 ( wx.Frame ):
            
         def __init__( self, parent ):
             wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 506,278 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )
                
             self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
                
             bSizer1 = wx.BoxSizer( wx.VERTICAL )
                
             self.m_button1 = wx.Button( self, wx.ID_ANY, u"点击1", wx.DefaultPosition, wx.DefaultSize, 0 )
             bSizer1.Add( self.m_button1, 1, wx.ALL, 5 )
                
             self.m_textCtrl1 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
             bSizer1.Add( self.m_textCtrl1, 3, wx.ALL|wx.EXPAND, 5 )
                
                
             self.SetSizer( bSizer1 )
             self.Layout()
                
             self.Centre( wx.BOTH )
            
         def __del__( self ):
             pass
    
     app = wx.App(False)
     frame = MyFrame1(None)
     frame.Show()
     app.MainLoop()
    

好了,运行一下,大家再用鼠标拖动frame的边界。可以发现 按钮和编辑框的大小也会随之改变了。


用界面生成工具还有一个好处,就是我们不需要记忆不同的界面控件对应的类的名字,直接在wxFormBuilder里面选控件就行了。


上面只是一个简单的界面布局的例子。 更复杂的界面,通常也是通过在Sizer层层嵌套,来完成界面的布局。

我们会在练习中给出这样的例子,让大家锻炼一下。




课后练习

去做练习