概述
MFC是我接触到的第一个界面库,当时的操作系统还是Windows95。在那个IT技术日新月异的年代,就像一个从荒蛮部落闯进文明社会的野人第一眼看见汽车那样,我对MFC充满了好奇和迷恋。尽管后来断断续续接触了WPF、Qt等GUI库,却始终对MFC情有独钟,以至于爱屋及乌,喜欢上了wxWidgets。
wxWidgets和MFC的确太相似了,连命名习惯和架构都高度相似。事实上,wxWidgets就是跨平台的MFC,对各个平台的差异做了抽象,后端还是用各平台原生的API实现。这正是wxWidgets的优点:编译出来的程序发行包比较小,性能也相当优异。
随着MFC的日渐式微,Qt异军突起,目前已成为最强大,最受欢迎的跨平台GUI库之一。在Python生态圈里,PyQt的用户群也远超wxPython。喜欢Qt的人认为这是技术竞争的结果,但我觉得这更像是开源理念和商业化思想的差异造成的。
wxWidgets像是一个孤独的勇士,高举开源的大旗,试图以一己之力构建一个相互承认、相互尊重的理想社会;而Qt则更像是一个在商业资本驱使下不断扩张的帝国,它不满足于封装不同平台的API,而是要创造出自己的API和框架,它不仅仅是UI,而是囊括了APP开发用到的所有东西,包括网络、数据库、多媒体、蓝牙、NFC、脚本引擎等。
缺少或拒绝商业化运作的支持,wxWidgets的悲情结局早已是命中注定。如果不是因为Python的兴盛和wxPython的复兴,wxWidgets也许早已经和MFC一样被遗忘在了角落里。不无夸张地说,wxPython是以MFC为代表的一个时代的挽歌,更是一曲理想主义的绝唱。
1.1 组织架构
其实,wxPython谈不上什么组织架构,因为桌面程序开发所用的类、控件、组件和常量几乎都被放到了顶级命名空间wx下面了。这样做看似杂乱无章,但用起来却是非常便捷。比如,导入必要的模块,PyQt通常要这样写:
PyQt巨人般的体量限制了使用星号导入所有的模块,只能用什么导入什么。而wxPython只需要简短的一句话:
再比如一些常量的写法,wxPython同样简洁,PyQt已经长到匪夷所思的程度了。比如左对齐和确定取消键,wxPython这样写:
PyQt写出来几乎要占一整行:
尽管wxPython也与时俱进地增加了一些诸如wx.xml、wx.svg之类地外围模块,但除了wx这个核心模块之外,我个人觉得只有wx.aui和wx.grid模块算是必要的扩展。如果想让界面更花哨点,那就要了解以下wx.adv、wx.ribbon这两个模块,纯python构建的控件库wx.lib也绝对值得一试。总之,站在我的应用领域看,wxPython的组织架构如下图所示。根据使用频率的高低,我给各个模块标注了红黄绿蓝四种颜色。
1.2 安装
截至本文写作时,wxPython的最新版本是4.1.1。Windows用户和macOS用户可以直接使用下面的命令安装。
由于Linux平台存在发行版之间的差异,必须使用相应的包管理器进行下载和安装。例如,在Ubuntu系统上可以尝试下面的安装命令。
快速体验
2.1 桌面应用程序开发的一般流程
用wxPython写一个桌面应用程序,通常分为6个步骤:
第1步:导入模块
第2步:创建一个应用程序
第3步:创建主窗口
第4步:在主窗口上实现业务逻辑
第5步:显示窗主口
第6步:应用程序进入事件处理主循环
除第4步之外的其它步骤,基本都是一行代码就可以完成,第4步的复杂程度取决于功能需求的多寡和业务逻辑的复杂度。下面这段代码就是这个一般流程的体现。
2.2 Hello World
实际应用wxPython开发桌面应用程序的的时候,上面这样的写法难以实现和管控复杂的业务逻辑,因而都是采用面向对象的应用方式。下面的代码演示了以OOP的方式使用wxPython,并且为窗口增加了标题和图标,设置了窗口尺寸和背景色,同时也给静态文本控件StaticText设置了字体字号。
代码中用到了一个.png格式的图像文件文件,想要运行这段代码的话,请先替换成本地文件。至于文件格式,SetIcon方法没有限定,常见的包括.ico和.jpg在内的图像格式都支持。代码运行界面如下图所示。
2.3 常用控件介绍
尽管wxPython的核心模块和扩展模块提供了数以百计的各式控件和组件,但真正常用且必不可少的控件只有为数不多的几个:
wx.Frame - 窗口
wx.Panel - 面板
wx.StaticText - 静态文本
StaticBitmap - 静态图片
wx.TextCtrl - 单行或多行文本输入框
wx.Button - 按钮
wx.RadioButton - 单选按钮
wx.CheckBox - 复选按钮
wx.Choice - 下拉选择框
所有的wxPython控件都有一个不可或缺的parent参数和若干关键字参数,通常,关键字参数都有缺省默认值。
parent - 父级对象
id - 控件的唯一标识符,缺省或-1表示自动生成
pos - 控件左上角在其父级对象上的绝对位置
size - 控件的宽和高
name - 用户定义的控件名
style - 控件风格
wxPython的控件在使用风格上保持着高度的一致性,一方面因为它们从一个共同的基类派生而来,更重要的一点,wxPython不像PyQt那样充斥着随处可见的重载函数。比如,PyQt的菜单栏QMenuBar增加菜单,就有addMenu(QMenu)、addMenu(str)和addMenu(QIcon, str)等三种不同的重载形式。方法重载固然带来了很多便利,但也会增加使用难度,让用户无所适从。
下面的代码演示了上述常用控件的使用方法。
代码运行界面如下图所示。
控件布局
3.1. 分区布局
上面的例子里,输入框、按钮等控件的位置由其pos参数确定,即绝对定位。绝对定位这种布局方式非常直观,但不能自动适应窗口的大小变化。更普遍的方式是使用被称为布局管理器的wx.Sizer来实现分区布局。所谓分区布局,就是将一个矩形区域沿水平或垂直方向分割成多个矩形区域,并可嵌套分区布局管理器wx.Sizer的派生类有很多种,最常用到是wx.BoxSizer和wx.StaticBoxSizer。
和一般的控件不同,布局管理器就像是一个魔法口袋:它是无形的,但可以装进不限数量的任意种类的控件——包括其他的布局管理器。当然,魔法口袋也不是万能的,它有一个限制条件:装到里面的东西,要么是水平排列的,要么是垂直排列的,不能排成方阵。好在程序员可以不受限制地使用魔法口袋,当我们需要排成方阵时,可以先每一行使用一个魔法口袋,然后再把所有的行装到一个魔法口袋中。
创建一个魔法口袋,装进几样东西,然后在窗口中显示的伪代码是这样的:
魔法口袋的 add() 方法总共有4个参数:第1个参数很容易理解,就是要装进口袋的物品;第2个参数和所有 add() 方法的第2个参数之和的比,表示装进口袋的物品占用空间的比例,0表示物品多大就占多大地儿,不额外占用空间;第3个参数相对复杂些,除了约定装进口袋的物品在其占用的空间里面水平垂直方向的对齐方式外,还可以指定上下左右四个方向中的一个或多个方向的留白(padding);第4个参数就是留白像素数。
下面是一个完整的例子。
代码运行界面如下图所示。
顾名思义,栅格布局就是将布局空间划分成网格,将控件放置到不同的网格内。栅格布局比较简单,用起来非常方便。栅格布局布局管理器也有很多种,GridBagSizer是最常用的一种。下面是一个使用GridBagSizer实现栅格布局的例子。
代码运行界面如下图所示。
事件驱动
一个桌面程序不单是控件的罗列,更重要的是对外部的刺激——包括用户的操作做出反应。如果把窗体和控件比作是桌面程序的躯体,那么响应外部刺激就是它的灵魂。wxPython的灵魂是事件驱动机制:当某事件发生时,程序就会自动执行预先设定的动作。
4.1 事件
所谓事件,就是我们的程序在运行中发生的事儿。事件可以是低级的用户动作,如鼠标移动或按键按下,也可以是高级的用户动作(定义在wxPython的窗口部件中的),如单击按钮或菜单选择。事件可以产生自系统,如关机,,也可以由用户自定义事件。
除了用户自定义事件,在wxPython中我习惯把事件分为4类:
鼠标事件:鼠标左右中键和滚轮动作,以及鼠标移动等事件
键盘事件:用户敲击键盘产生的事件
控件事件:发生在控件上的事件,比如按钮被按下、输入框内容改变等
系统事件:关闭窗口、改变窗口大小、重绘、定时器等事件
事实上,这个分类方法不够严谨。比如,wx.Frame作为一个控件,关闭和改变大小也是控件事件,不过这一类事件通常都由系统绑定了行为。基于此,可以重新定义所谓的控件事件,是指发生在控件上的、系统并未预定义行为的事件。
常用的鼠标事件包括:
wx.EVT_LEFT_DOWN - 左键按下
wx.EVT_LEFT_UP - 左键弹起
wx.EVT_LEFT_DCLICK - 左键双击
wx.EVT_RIGHT_DOWN - 右键按下
wx.EVT_RIGHT_UP - 右键弹起
wx.EVT_RIGHT_DCLICK - 右键双击
wx.EVT_MOTION - 鼠标移动
wx.EVT_MOUSEWHEEL - 滚轮滚动
wx.EVT_MOUSE_EVENTS - 所有的鼠标事件
常用的键盘事件有:
wx.EVT_KEY_DOWN - 按键按下
wx.EVT_KEY_UP - 按键弹起
常用的系统事件包括:
wx.EVT_CLOSE - 关闭
wx.EVT_SIZE - 改变大小
wx.EVT_TIMER - 定时器事件
wx.EVT_PAINT - 重绘
wx.EVT_ERASE_BACKGROUND -背景擦除
常用的控件事件包括:
wx.EVT_BUTTON - 点击按钮
wx.EVT_CHOICE - 下拉框改变选择
wx.EVT_TEXT - 输入框内容改变
wx.EVT_TEXT_ENTER - 输入框回车
wx.EVT_RADIOBOX - 单选框改变选择
wx.EVT_CHECKBOX - 点击复选框
4.2 事件绑定
事件驱动机制有三个要素:事件、事件函数和事件绑定。比如,当一个按钮被点击时,就会触发按钮点击事件,该事件如果绑定了事件函数,事件函数就会被调用。所有的事件函数都以事件对象为参数,事件对象提供了事件的详细信息,比如键盘按下事件的事件对象就包含了被按下的键的信息。
下面这个例子演示了如何定义事件函数,以及绑定事件和事件函数之间的关联关系。
代码运行界面如下图所示。
两个输入框,一个明文居中,一个密写右齐,但内容始终保持同步。输入焦点不在输入框的时候,敲击键盘,界面显示对应的键值。最上面的按钮响应鼠标左键的按下和弹起事件,中间的按钮响应所有的鼠标事件,下面的按钮响应滚轮事件和按钮按下的事件。另外,程序还绑定了窗口关闭事件,重新定义了关闭函数,增加了确认选择。
程序框架
5.1 菜单栏、工具栏和状态栏
通常,一个完整的窗口程序一般都有菜单栏、工具栏和状态栏。下面的代码演示了如何创建菜单栏、工具栏和状态栏,顺便演示了类的静态属性的定义和用法。不过,说实话,wx的工具栏有点丑,幸好,wx还有一个 AUI 的工具栏比较漂亮,我会在后面的例子里演示它的用法。
代码里面用到了4个16x16的工具按钮,请自备4个图片文件,保存路径请查看代码中的注释。代码运行界面如下图所示。
5.2 Aui框架
Advanced User Interface,简称AUI,是wxPython的子模块,使用AUI可以方便地开发出美观、易用的用户界面。从2.8.9.2版本之后,wxPython增加了一个高级通用部件库Advanced Generic Widgets,简称AGW库, AGW库也提供了AUI模块 wx.lib.agw.aui,而 wx.aui也依然保留着。相比较而言,我更喜欢使用wx.lib.agw的AUI框架。
使用AUI框架可以概括为以下四步:
创建一个布局管理器:mgr = aui.AuiManager()
告诉主窗口由mgr来管理界面:mgr.SetManagedWindow()
添加界面上的各个区域:mgr.AddPane()
更新界面显示:mgr.Update()
下面的代码演示了如何使用AUI布局管理器创建和管理窗口界面。
代码运行界面如下图所示。
前文的例子中已经展示了wx.StaticBitmap控件作为图像容器的例子,下面的例子用它制作了一个相册,点击前翻后翻按钮可在多张照片之间循环切换。
代码运行界面如下图所示。
几乎所有的GUI课程都会用计算器作为例子,wxPython怎能缺席呢?下面这个计算器除了常规的计算外,按下每个键都会发出不同的音调,粗通乐理就可以弹奏出乐曲。此外,代码中使用了wx.lib控件库的按键,略带3D风格。
代码运行界面如下图所示。
6.3. 定时器和线程
在一个桌面程序中,GUI线程是主线程,其他线程若要更新显示内容,Tkinter使用的是类型对象,PyQt使用的信号和槽机制,wxPython则相对原始:它允许子线程更新GUI,但需要借助于wx.CallAfter()函数。
这个例子里面设计了一个数字式钟表,一个秒表,秒表显示精度十分之一毫秒。从代码设计上来说没有任何难度,实现的方法有很多种,可想要达到一个较好的显示效果,却不是一件容易的事情。请注意体会 wx.CallAfter() 的使用条件。
代码运行界面如下图所示。界面上方的时钟一直再跑,下方的秒表则是按键启动或停止。
6.4. DC绘图
DC 是 Device Context 的缩写,字面意思是设备上下文——我一直不能正确理解DC这个中文名字,也找不到更合适的说法,所以,我坚持使用DC而不是设备上下文。DC可以在屏幕上绘制点线面,当然也可以绘制文本和图像。事实上,在底层所有控件都是以位图形式绘制在屏幕上的,这意味着,我们一旦掌握了DC这个工具,就可以自己创造我们想要的控件了
DC有很多种,PaintDC,ClientDC,MemoryDC等。通常,我们可以使用 ClientDC 和 MemoryDC,PaintDC 是发生重绘事件(wx.EVT_PAINT)时系统使用的。使用 ClientDC 绘图时,需要记录绘制的每一步工作,不然,系统重绘时会令我们前功尽弃——这是使用DC最容易犯的错误。
代码运行界面如下图所示。
6.5. 内嵌浏览器
wx.html2是wxPython扩展模块中封装得最干净漂亮的模块之一,它被设计为允许为每个端口创建多个后端,尽管目前只有一个可用。它与wx.html.HtmlWindow的不同之处在于,每个后端实际上都是一个完整的渲染引擎,MSW上是Trident, macOS和GTK上是Webkit。wx.html2渲染web文档,对于HTML、CSS和javascript都可以有很好的支持。
代码运行界面如下图所示。
集成应用
7.1. 集成Matplotlib
Matplotlib的后端子模块backends几乎支持所有的GUI库,wxPyton当然也不例外,backend_wxagg是专门为wxPyton生成canvas的类,只要传一个matplotlib.Figure实例即可。剩下的就是水到渠成了。
代码运行界面如下图所示。
7.2. 集成OpenGL
wx.glcanvas.GLCanvas是wxPython为显示OpenGL提供的类,顾名思义,可以将其理解为OpenGL的画板。有了这个画板,我们就可以使用OpenGL提供的各种工具在上面绘制各种三维模型了。下面的代码仅是一个demo,并未构建投影系统和视点系统。
代码运行界面如下图所示。