wxpython 是什么

wxpython是用Python写的跨平台GUI工具,通俗的理解就是用来写软件界面的包。与之功能类似的Python包有 PyQtTkinterPyGtk等等,其中PyQt内容丰富,功能强大,网上资源也很多,但是上手难度较大。我选择的wxpython上手相对简单,但是网上资源较少,实现相关功能时需要在参考官方文档。

关于这篇笔记不会从hello world 开始介绍wxpyhton,我会直接介绍我学习完后的一些经验,由于我涉及的功能并没有完全发挥出wxpython的强大功能,在这里给一些学习方案,新手大神均可食用

  1. 提高:官方案例库:https://extras.wxpython.org/wxPython4/extras/4.0.0/
  2. 进阶:官网:https://wxpython.org/pages/downloads/

安装 wxpython

pip install -U wxPython

wxpyhton 基础知识

了解界面构成

常见的GUI界面包含五个部分:主界面(main window), 菜单页面(menu), 按键(button), 标签(labels), 文本输入(text entry),如下图所示,图中多了一个画布。

wxpython API中文手册 wxpy python_事件绑定

wxpython中,为了更好的实现GUI编写,封装了多个类,常用的除了有wx.Frame(对应主界面)、wx.MenuBar()(对应菜单类)、wx.Button(对应按键)、wx.StaticText(对应label)、wx.TextCtrl(对应text entry)、wx.lib.plot.PlotCanvas(对应画布)与上图对应之外,还有wx.Panel,可以按照下图的方式理解,也就是一个Frame可以包含多个PanelPanel里面可以包含label、text、plot但是一个Frame只能有一个Menu!!

wxpython API中文手册 wxpy python_python_02

另外还有一个重要的类为wx.Dialog,如下图所示,Dialog可以包含多个Panel,常用来实现选项界面,在这里我用来实现串口设置界面。

wxpython API中文手册 wxpy python_事件绑定_03

根据上述的结构,实现的伪代码如下所示

class Dialog(Wx.Dialog):
    """
    实现串口设置
    """

class Plane1(wx.Panel):
    """
    包含3个按钮、以及label(文字log)和text
    """

class Planel3(wx.Panel):
    """
    包含多个label 和 text
    """

calss MainFrame(wx.Frame):
    """
    主界面
    """ 
    self.mainmenu = wx.MenuBar() # 注册菜单界面
    self.panel_setting = Planel1(self, size=(700,100)) # 注册Panel 1
    self.panel_data_pre = wxplot.PlotCanvas(self, size=(700,300)) #注册Panel2→画布1
    self.panel_control = Planel3(self, size=(700,100)) # 注册Panel 3
    self.panel_data_pro = wxplot.PlotCanvas(self, size=(700,300)) # 注册Panel4→画布2 

if __name__ == '__main__':
    app = wx.App()
    frame = MainFrame(ser) # 将主界面注册到 App中
    frame.Show()
    app.SetTopWindow(frame)
    app.MainLoop() # 运行App

界面布局

了解完界面构成后,下一步需要实现界面布局。界面布局用到的类为wx.BoxSizerwx.FlexGridSizer

wx.BoxSizer表示初始化一个可以设置大小区域,该类的参数orient默认参数为 HORIZONTAL,也就是水平分布,简单示例如下所示

import wx

class DemoPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
 
        button1 = wx.Button(self, label='1') # 生成按键1,
        button2 = wx.Button(self, label='2')
        
        boxsier = wx.BoxSizer(HORIZONTAL)# 生成可设置大小的BoxSizer,并且初始方案为水平分布
        boxsier.Add(button1) # 注册 按键1 
        boxsier.Add(button2)

        self.SetSizer(boxsier)


class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Demo.exe')

        demopanel = DemoPanel(self)    
        self.Show()


if __name__ == '__main__':
    app = wx.App()
    frame = MainFrame()
    app.SetTopWindow(frame)
    app.MainLoop()

结果为:

wxpython API中文手册 wxpy python_wxpython_04

如果修改参数orient=VERTICAL

class DemoPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)

        button1 = wx.Button(self, label='1')
        button2 = wx.Button(self, label='2')
        
        boxsier = wx.BoxSizer(VERTICAL)
        boxsier.Add(button1)
        boxsier.Add(button2)

        self.SetSizer(boxsier)

其结果为

wxpython API中文手册 wxpy python_事件绑定_05

更多参数了解参考:https://docs.wxpython.org/wx.BoxSizer.html?highlight=boxsizer#wx.BoxSizer

wx.FlexGridSizer为一个经常使用的将多个部件 规则布局的类,使用如下所示。 其中row和cols表示行和列的个数,vgap和hgap分别表示垂直和水平间距

class DemoPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)

        button1 = wx.Button(self, label='1')
        button2 = wx.Button(self, label='2')
        button3 = wx.Button(self, label='3')
        button4 = wx.Button(self, label='4')
        
        boxsier = wx.BoxSizer(VERTICAL)
        flex = wx.FlexGridSizer(rows=4, cols=1, vgap=5, hgap=5) # 生成4行1列的结构
        flex.Add(button1)
        flex.Add(button2)
        flex.Add(button3)
        flex.Add(button4)
        boxsier.Add(flex)
        self.SetSizer(boxsier)

结果为:

wxpython API中文手册 wxpy python_App_06

如果修改参数 rowcols

flex = wx.FlexGridSizer(rows=2, cols=2, vgap=5, hgap=5)

结果就改为2行2列

wxpython API中文手册 wxpy python_App_07

关于wx.FlexGridSizer更多细节了解参考https://docs.wxpython.org/wx.FlexGridSizer.html?highlight=flexgridsizer#wx.FlexGridSizer

在注册按键中使用了函数 Add,其函数参数有``Add(window, proportion=0, flag=0, border=0, userData=None*)。在使用中,flag=wx.CENTER`表示将该组件居中,常用的位置flag有参数有

wx.TOP   # 表示与主sizer 哪条边有border的距离
wx.BOTTOM
wx.LEFT
wx.RIGHT
wx.ALL  
wx.EXPAND # 扩展到整个可使用的sizer
wx.ALIGN_CENTER or wx.ALIGN_CENTRE
wx.ALIGN_LEFT
wx.ALIGN_RIGHT
wx.ALIGN_RIGHT
wx.ALIGN_TOP
wx.ALIGN_BOTTOM #对齐方式

需要注意的是border参数与flag一起配合使用,如Add(self, 1, wx.CENTER|wx.LEFT, 5)表示该组件与中间对齐并且距离左边界5个像素单位。具体使用参考https://docs.wxpython.org/sizers_overview.html?highlight=add#Add

事件绑定

事件绑定也就是将组件和事件联系起来,比如实现按键关闭窗口,就是通过将按键❌与关闭窗口函数绑定进行实现的。wxpython的事件绑定函数为Bind,其使用方法如下所示

class DemoPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        
        button1 = wx.Button(self, label='1')
		button1.Bind(wx.EVT_BUTTON, self.button1_hander)
        
    
    def button1_hander(self, event):
        print('hello word')

实现效果为,按下按键1,控制台打印’hello world’

wxpython API中文手册 wxpy python_wxpython API中文手册_08

在绑定事件中还会用到参数id,每个组件在初始化的时候都会有一个id与之绑定,如果没有定义的话系统会随机赋值一个,为了方便事件绑定,一般会在代码开头自定义组件id,比如

from wx.lib import newevent as wxnewevent

ID_BUTTON = wx.NewId()

class DemoPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        
        button1 = wx.Button(self, label='1', id=ID_BUTTON) # 将button的id绑定
        self.Bind(wx.EVT_BUTTON, self.button1_hander, id=ID_BUTTON)
     def button1_hander(self, event):
        print('hello word')

该代码与最开始的事件绑定代码实现效果相同。

wxpython 作函数图

在网上有方案是通过将wxpython和Matplotlib拼接起来实现在GUI中作图,但是这一点也不pythonic。我在官网找到了官方给的作图方案:https://docs.wxpython.org/wx.lib.plot.plotcanvas.PlotCanvas.html#wx.lib.plot.plotcanvas.PlotCanvas

其中官方的作图例程很丰富

在wxpython中作图流程为:注册画布Panel(也就是PlotCanvas)→在画布上注册坐标图(PlotGraphics)→在坐标图中注册函数图像(PolyLine、PolySpline、PolyHistogram、PolyBars、PolyMarker)。使用参考如下代码

def _draw1Objects():
    """Sin, Cos, and Points"""
    # PolyMarker 图
    data1 = 2. * np.pi * np.arange(-200, 200) / 200.
    data1.shape = (200, 2)
    data1[:, 1] = np.sin(data1[:, 0])
    markers1 = wxplot.PolyMarker(data1,
                                 legend='Green Markers',
                                 colour='green',
                                 marker='circle',
                                 size=1,
                                 )

    # PolyMarker 图
    data1 = 2. * np.pi * np.arange(-100, 100) / 100.
    data1.shape = (100, 2)
    data1[:, 1] = np.cos(data1[:, 0])
    lines = wxplot.PolySpline(data1, legend='Red Line', colour='red')
    markers3 = wxplot.PolyMarker(data1,
                                 legend='Red Dot',
                                 colour='red',
                                 marker='circle',
                                 size=1,
                                 )

    # PolyMarker 图
    pi = np.pi
    pts = [(0., 0.), (pi / 4., 1.), (pi / 2, 0.), (3. * pi / 4., -1)]
    markers2 = wxplot.PolyMarker(pts,
                                 legend='Cross Legend',
                                 colour='blue',
                                 marker='cross',
                                 )
    line2 = wxplot.PolyLine(pts, drawstyle='steps-post')
	# 将函数图像 注册到坐标图中
    return wxplot.PlotGraphics([markers1, lines, markers3, markers2, line2],
                               "Graph Title",
                               "X Axis",
                               "Y Axis",
                               )

class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Demo.exe')
        self.panel_canvas = wxplot.PlotCanvas(self, size=(700,300)) #将坐标图注册到画布中
        self.panel_canvas.Draw(_draw1Objects()) 
        self.Show()
        
if __name__ == '__main__':
    app = wx.App()
    frame = MainFrame()
    app.SetTopWindow(frame)
    app.MainLoop()

结果如下所示

wxpython API中文手册 wxpy python_python_09

函数实现可选择

函数图像选择的逻辑是:获取鼠标点击的坐标→取得坐标在y轴数据中的索引区间→将索引区间数据取出来重新画图。实现这个操作的关键在于函数np.searchsorted,函数官网https://numpy.org/doc/stable/reference/generated/numpy.searchsorted.html#numpy.searchsorted

numpy.searchsorted(a,v,side='left',sorter=None)返回将v插入a中后的索引值,前提是a是排好序的!side 表示插入值满足的条件。

side

returned index i satisfies

left

a[i-1] < v <= a[i]

right

a[i-1] <= v < a[i]

>>> np.searchsorted([1,2,3,4,5], 3)
2 #将3插入[1,2,3,4,5]列表中的索引值为2
>>> np.searchsorted([1,2,3,4,5], 3, side='right')
3 # 插入值3 满足 3(list[1,2,3,4,5]中的3)<= 3 < 4
>>> np.searchsorted([1,2,3,4,5], [-10, 10, 2, 3])
array([0, 5, 1, 2]) # 返回每个值在list[1,2,3,4,5]插入的索引值

具体使用参考案例:Span Selector

wxpython 中的定时器

让wxpython动起来需要wx.Timer()模块和多线程模块。

wx.Timer()可以实现定时刷新界面,使用如下代码

class MainFrame(wx.Frame):

    def __init__(self):
        super().__init__(None, title='Demo.exe',size=(900, 600))
		
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)  # 刷新事件绑定
        self.timer.Start(1000) # 1s更新一次 Frame 
        
    def OnTimer(self, event):
       	# 再绑定事件中更新
        data_length = len(self.pre_mean_data)
        self.x = [i+1 for i in range(data_length) ]
        self.y = self.pre_mean_data
        # 更新采样总数
 
        self.Draw() # 更新画图函数
    
    def Draw(self):
        pass  # 画图函数

wxpython 中的多线程

wxpython内置有多线程模块,但是好像不太好使,所以需要多线程时使用 threading模块,使用方法如下

import threading

class MainFrame(wx.Frame):

    def __init__(self):
        super().__init__(None, title='Demo.exe',size=(900, 600))
        
        # 初始化线程
        self.thread = None
        self.alive = threading.Event() # 创建线程之间的通讯
        
    def StartThread(self):
        self.thread = threading.Thread(target=self.ComPortThread)
        self.thread.setDaemon(1)  # 保证主线程结束时,子线程也结束
        self.thread.start() # 开始线程任务
        
        self.alive.set()  # 保证线程通讯开启
        
    def StopThread(self):
        if self.thread is not None:
            self.alive.clear() # 关闭线程通讯
            self.thread.join() # 等待线程运行结束后结束线程
            self.thread = None  
            
     def ComPortThread(self):
        """读取串口函数"""

打包 .py 为 .exe 文件

.py文件打包为.exe有很多方法,我这里使用的是pyinstaller, 需要安装合适版本的模块pip install pyinstaller 。安装pyinstaller有很多的坑,在网上可以找到很多案例,这里记录两个我遇到的坑

PermissionError: [Errno 13] Permission denied:

解决办法:安装pywin32,然后用管理员权限打开cmd,运行打包程序

win32ctypes.pywin32.pywintypes.error: (110, 'EndUpdateResourceW', '系统无法打开指定的设备或文件。')

解决办法:安装别的版本的 pyinstaller, 我默认安装的是4.7,卸载后安装4.6解决问题。pip install pyinstaller version=4.6

虽然安装有很多坑,但是使用起来很快乐,一句话就可以pyinstaller main.py --onefile -w --noconsole, 这里的main.py是我要打包的py文件, --onefile参数表示只生成一个exe文件!--noconsole表示运行exe文件时不显示控制台。

wxpython API中文手册 wxpy python_wxpython_10