wxpython 是什么
wxpython
是用Python写的跨平台GUI工具,通俗的理解就是用来写软件界面的包。与之功能类似的Python包有 PyQt
、Tkinter
、PyGtk
等等,其中PyQt
内容丰富,功能强大,网上资源也很多,但是上手难度较大。我选择的wxpython
上手相对简单,但是网上资源较少,实现相关功能时需要在参考官方文档。
关于这篇笔记不会从hello world 开始介绍wxpyhton,我会直接介绍我学习完后的一些经验,由于我涉及的功能并没有完全发挥出wxpython的强大功能,在这里给一些学习方案,新手大神均可食用
- 提高:官方案例库:https://extras.wxpython.org/wxPython4/extras/4.0.0/
- 进阶:官网:https://wxpython.org/pages/downloads/
安装 wxpython
pip install -U wxPython
wxpyhton 基础知识
了解界面构成
常见的GUI界面包含五个部分:主界面(main window), 菜单页面(menu), 按键(button), 标签(labels), 文本输入(text entry),如下图所示,图中多了一个画布。
在wxpython
中,为了更好的实现GUI编写,封装了多个类,常用的除了有wx.Frame
(对应主界面)、wx.MenuBar()
(对应菜单类)、wx.Button
(对应按键)、wx.StaticText
(对应label)、wx.TextCtrl
(对应text entry)、wx.lib.plot.PlotCanvas
(对应画布)与上图对应之外,还有wx.Panel
,可以按照下图的方式理解,也就是一个Frame可以包含多个Panel,Panel里面可以包含label、text、plot,但是一个Frame只能有一个Menu!!
另外还有一个重要的类为wx.Dialog
,如下图所示,Dialog可以包含多个Panel,常用来实现选项界面,在这里我用来实现串口设置界面。
根据上述的结构,实现的伪代码如下所示
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.BoxSizer
和wx.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()
结果为:
如果修改参数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)
其结果为
更多参数了解参考: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)
结果为:
如果修改参数 row
和cols
flex = wx.FlexGridSizer(rows=2, cols=2, vgap=5, hgap=5)
结果就改为2行2列
关于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’
在绑定事件中还会用到参数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()
结果如下所示
函数实现可选择
函数图像选择的逻辑是:获取鼠标点击的坐标→取得坐标在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 |
|
right |
|
>>> 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文件时不显示控制台。