
作者|许向武出品|CSDN博客概述MFC是我接触到的第一个界面库,当时的操作系统还是Windows95。在那个IT技术日新月异的年代,就像一个从荒蛮部落闯进文明社会的野人第一眼看见汽车那样,我对MFC......
作者|许向武
出品|CSDN博客

概述
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通常要这样写:
,QWidget,QComboBox,QPushButton,QHBoxLayout,QVBoxLayout,QColorDialog,QPainter,QPen,QColor,QPolygon,QPoint
PyQt巨人般的体量限制了使用星号导入所有的模块,只能用什么导入什么。而wxPython只需要简短的一句话:
importwx
再比如一些常量的写法,wxPython同样简洁,PyQt已经长到匪夷所思的程度了。比如左对齐和确定取消键,wxPython这样写:
_LEFT|
PyQt写出来几乎要占一整行:
|
尽管wxPython也与时俱进地增加了一些诸如、之类地外围模块,但除了wx这个核心模块之外,我个人觉得只有和模块算是必要的扩展。如果想让界面更花哨点,那就要了解以下、这两个模块,纯python构建的控件库也绝对值得一试。总之,站在我的应用领域看,wxPython的组织架构如下图所示。根据使用频率的高低,我给各个模块标注了红黄绿蓝四种颜色。

1.2安装
截至本文写作时,wxPython的最新版本是4.1.1。Windows用户和macOS用户可以直接使用下面的命令安装。
pipinstall-UwxPython
由于Linux平台存在发行版之间的差异,必须使用相应的包管理器进行下载和安装。例如,在Ubuntu系统上可以尝试下面的安装命令。

快速体验
2.1桌面应用程序开发的一般流程
用wxPython写一个桌面应用程序,通常分为6个步骤:
第1步:导入模块
第2步:创建一个应用程序
第3步:创建主窗口
第4步:在主窗口上实现业务逻辑
第5步:显示窗主口
第6步:应用程序进入事件处理主循环
除第4步之外的其它步骤,基本都是一行代码就可以完成,第4步的复杂程度取决于功能需求的多寡和业务逻辑的复杂度。下面这段代码就是这个一般流程的体现。
第2步:创建一个应用程序app=()
第4步:在主窗口上实现业务逻辑st=(frame,-1,'HelloWorld')
第6步:应用程序进入事件处理主循环()
2.2HelloWorld
实际应用wxPython开发桌面应用程序的的时候,上面这样的写法难以实现和管控复杂的业务逻辑,因而都是采用面向对象的应用方式。下面的代码演示了以OOP的方式使用wxPython,并且为窗口增加了标题和图标,设置了窗口尺寸和背景色,同时也给静态文本控件StaticText设置了字体字号。
importwx
classMainFrame():"""从派生主窗口类"""def__init__(self,parent):"""构造函数"""__init__(self,parent,-1,style=_FRAME_STYLE)('最简的的应用程序')(('res/'))设置窗口背景色((300,80))窗口在屏幕上居中st=(self,-1,'HelloWorld',style=_CENTER)设置字体字号
if__name__=='__main__':app=()创建主窗口()应用程序进入事件处理主循环
代码中用到了一个.png格式的图像文件文件,想要运行这段代码的话,请先替换成本地文件。至于文件格式,SetIcon方法没有限定,常见的包括.ico和.jpg在内的图像格式都支持。代码运行界面如下图所示。

2.3常用控件介绍
尽管wxPython的核心模块和扩展模块提供了数以百计的各式控件和组件,但真正常用且必不可少的控件只有为数不多的几个:
窗口
面板
静态文本
StaticBitmap-静态图片
单行或多行文本输入框
按钮
单选按钮
复选按钮
下拉选择框
所有的wxPython控件都有一个不可或缺的parent参数和若干关键字参数,通常,关键字参数都有缺省默认值。
parent-父级对象
id-控件的唯一标识符,缺省或-1表示自动生成
pos-控件左上角在其父级对象上的绝对位置
size-控件的宽和高
name-用户定义的控件名
style-控件风格
wxPython的控件在使用风格上保持着高度的一致性,一方面因为它们从一个共同的基类派生而来,更重要的一点,wxPython不像PyQt那样充斥着随处可见的重载函数。比如,PyQt的菜单栏QMenuBar增加菜单,就有addMenu(QMenu)、addMenu(str)和addMenu(QIcon,str)等三种不同的重载形式。方法重载固然带来了很多便利,但也会增加使用难度,让用户无所适从。
下面的代码演示了上述常用控件的使用方法。
importwx
classMainFrame():"""从派生主窗口类"""def__init__(self,parent):"""构造函数"""创建一个面板,用于放置控件panel=(self,-1)
在x=300,y=20的位置,创建静态图片bmp=('res/')sb=(panel,-1,bmp,pos=(280,10))
在x=20,y=90的位置,创建文本输入框,指定样式为密码tc2=(panel,-1,value='我是密码',pos=(20,90),style=_PASSWORD)
在x=100,y=130的位置,创建单选按钮,不再需要指定样式_GROUPrb2=(panel,-1,'单选按钮2',pos=(100,130),name='rb2')
在x=20,y=160的位置,创建复选按钮cb1=(panel,-1,'复选按钮',pos=(20,160))
在x=20,y=190的位置,创建按钮ch=(panel,-1,choices=['wxPython','PyQt','Tkinter'],pos=(20,190),size=(100,-1))(0)在x=20,y=230的位置,创建文本框,指定大小为260*150,并指定其样式为多行和只读tc3=(panel,-1,value='我是多行文本输入框',pos=(20,230),size=(260,150),style=_MULTILINE|_READONLY)
if__name__=='__main__':app=()创建主窗口()应用程序进入事件处理主循环
代码运行界面如下图所示。


控件布局
3.1.分区布局
上面的例子里,输入框、按钮等控件的位置由其pos参数确定,即绝对定位。绝对定位这种布局方式非常直观,但不能自动适应窗口的大小变化。更普遍的方式是使用被称为布局管理器的来实现分区布局。所谓分区布局,就是将一个矩形区域沿水平或垂直方向分割成多个矩形区域,并可嵌套分区布局管理器的派生类有很多种,最常用到是和。
和一般的控件不同,布局管理器就像是一个魔法口袋:它是无形的,但可以装进不限数量的任意种类的控件——包括其他的布局管理器。当然,魔法口袋也不是万能的,它有一个限制条件:装到里面的东西,要么是水平排列的,要么是垂直排列的,不能排成方阵。好在程序员可以不受限制地使用魔法口袋,当我们需要排成方阵时,可以先每一行使用一个魔法口袋,然后再把所有的行装到一个魔法口袋中。
创建一个魔法口袋,装进几样东西,然后在窗口中显示的伪代码是这样的:
魔法口袋=()装入确认按钮魔法口袋.add(取消按钮,0,,0)把魔法口袋放到窗口上窗口.Layout()设置窗口大小self._init_ui()窗口在屏幕上居中def_init_ui(self):"""初始化界面"""生成黑色背景的预览面板view=(panel,-1,style=_BORDER)((0,0,0))左右按钮装入一个水平布局管理器sizer_arrow_mid=()sizer_arrow_(btn_left,0,,16)sizer_arrow_(btn_right,0,,16)装入上按钮sizer_(sizer_arrow_mid,0,|,1)装入下按钮装入拍照按钮sizer_(sizer_arrow,0,_CENTER|,0)装入多行文本控件装入左侧的预览面板sizer_(sizer_right,0,|,0)为容器面板指定布局管理器,并调用布局方法完成界面布局(sizer_max)()
if__name__=='__main__':app=()frame=MainFrame(None)()()
代码运行界面如下图所示。

顾名思义,栅格布局就是将布局空间划分成网格,将控件放置到不同的网格内。栅格布局比较简单,用起来非常方便。栅格布局布局管理器也有很多种,GridBagSizer是最常用的一种。下面是一个使用GridBagSizer实现栅格布局的例子。
importwx
classMainFrame():"""从派生主窗口类"""def__init__(self,parent):"""构造函数"""__init__(self,parent,style=_FRAME_STYLE)('栅格布局')(('res/'))((800,440))初始化界面()创建容器面板sizer=(10,10)在第0行0列,距离上边缘20像素,右对齐
userName=(panel,-1)(userName,(0,1),(1,3),flag=|,border=20)在第0行4列,跨7行,距离上右边缘20像素
st=(panel,-1,"密码")(st,(1,0),flag=_RIGHT)在第1行1列,跨3列
st=(panel,-1,"学历")(st,(2,0),flag=_RIGHT)在第2行1列
level2=(panel,-1,"本科")(level2,(2,2))在第2行1列
st=(panel,-1,"职业")(st,(3,0),flag=_RIGHT)在第3行1列,跨3列
在第4行0列,距离左边缘20像素,右对齐
choices=["C","C++","Java","Python","Lua","JavaScript","TypeScript","Go","Rust"]language=(panel,-1,choices=choices,style=_EXTENDED)(language,(4,1),(1,3),flag=)在第5行0列,跨4列,居中
btn=(panel,-1,"提交")(btn,(6,0),(1,4),flag=_CENTER|,border=20)设置第4行可增长(3)设置窗口背景色((520,220))self._init_ui()()def_init_ui(self):"""初始化界面"""(self,-1,'第一行输入框:',pos=(40,50),size=(100,-1),style=_RIGHT)(self,-1,'第二行输入框:',pos=(40,80),size=(100,-1),style=_RIGHT)=(self,-1,u'',pos=(145,110),size=(150,-1),style=_NO_AUTORESIZE)=(self,-1,'',pos=(145,50),size=(150,-1),name='TC01',style=_CENTER)=(self,-1,'',pos=(145,80),size=(150,-1),name='TC02',style=_PASSWORD|_RIGHT)btn_mea=(self,-1,'鼠标左键事件',pos=(350,50),size=(100,25))btn_meb=(self,-1,'鼠标所有事件',pos=(350,80),size=(100,25))btn_close=(self,-1,'关闭窗口',pos=(350,110),size=(100,25))(_TEXT,_text)绑定文本内容改变事件btn_(_BUTTON,_close,btn_close)绑定鼠标滚轮事件btn_(_LEFT_DOWN,_left_down)绑定鼠标左键弹起btn_(_MOUSE_EVENTS,_mouse)绑定窗口关闭事件(_SIZE,_size)绑定键盘事件defon_text(self,evt):"""输入框事件函数"""obj=()objName=()text=()ifobjName=='TC01':(text)elifobjName=='TC02':(text)defon_size(self,evt):'''改变窗口大小事件函数'''print('你想改变窗口,但是事件被Skip了,所以没有任何改变')()设置窗口背景色((360,180))self._create_menubar()工具栏self._create_statusbar()文件菜单m=()(_open,'打开文件')(_save,'保存文件')()(_quit,'退出系统')(m,'文件')(_MENU,_open,id=_open)(_MENU,_save,id=_save)(_MENU,_quit,id=_quit)请自备按钮图片bmp_save=('res/save_',_TYPE_ANY)请自备按钮图片bmp_about=('res/info_',_TYPE_ANY)设置窗口背景色((640,480))self._init_ui()()def_init_ui(self):"""初始化界面"""=self._create_toolbar()=self._create_toolbar()=self._create_toolbar('V')p_left=(self,-1)p_center0=(self,-1)p_center1=(self,-1)p_bottom=(self,-1)btn=(p_left,-1,'切换',pos=(30,200),size=(100,-1))(_BUTTON,_switch)text0=(p_center0,-1,'我是第1页',pos=(40,100),size=(200,-1),style=_LEFT)text1=(p_center1,-1,'我是第2页',pos=(40,100),size=(200,-1),style=_LEFT)self._mgr=()self._(self)self._(,().Name('ToolBar1').Caption('工具条').ToolbarPane().Top().Row(0).Position(0).Floatable(False))self._(,().Name('ToolBar2').Caption('工具条').ToolbarPane().Top().Row(0).Position(1).Floatable(True))self._(,().Name('ToolBarV').Caption('工具条').ToolbarPane().Right().Floatable(True))self._(p_left,().Name('LeftPanel').Left().Layer(1).MinSize((200,-1)).Caption('操作区').MinimizeButton(True).MaximizeButton(True).CloseButton(True))self._(p_center0,().Name('CenterPanel0').CenterPane().Show())self._(p_center1,().Name('CenterPanel1').CenterPane().Hide())self._(p_bottom,().Name('BottomPanel').Bottom().MinSize((-1,100)).Caption('消息区').CaptionVisible(False).Resizable(True))self._()def_create_toolbar(self,d='H'):"""创建工具栏"""bmp_open=('res/open_',_TYPE_ANY)bmp_save=('res/save_',_TYPE_ANY)bmp_help=('res/help_',_TYPE_ANY)bmp_about=('res/info_',_TYPE_ANY)()in['V','VERTICAL']:tb=(self,-1,,,agwStyle=_TB_TEXT|_TB_VERTICAL)else:tb=(self,-1,,,agwStyle=_TB_TEXT)((16,16))(_open,'打开',bmp_open,'打开文件')(_save,'保存',bmp_save,'保存文件')()(_help,'帮助',bmp_help,'帮助')(_about,'关于',bmp_about,'关于')()returntbdefon_switch(self,evt):"""切换信息显示窗口"""p0=self._('CenterPanel0')p1=self._('CenterPanel1')(())(())self._()
if__name__=='__main__':app=()frame=MainFrame(None)()()
代码运行界面如下图所示。


importwx
classMainFrame():"""从派生主窗口类"""def__init__(self,parent):"""构造函数"""__init__(self,parent,style=_FRAME_STYLE)('相册')(('res/'))((224,224,224))定义按键排列顺序和名称keys=[['(',')','Back','Clear'],['7','8','9','/'],['4','5','6','*'],['1','2','3','-'],['0','.','=','+']]用输入框控件作为计算器屏幕,设置为只读(_READONLY)和右齐(_RIGHT)=(self,-1,'',pos=(10,10),size=(252,45),style=_READONLY|_RIGHT)((20,,,,False,'微软雅黑'))设置屏幕背景色((0,255,0))按键布局参数btn_size=(60,30)定义按键区域的相对位置dx,dy=(64,34)生成所有按键foriinrange(len(keys)):forjinrange(len(keys[i])):key=keys[i][j]btn=(self,-1,key,pos=(x0+j*dx,y0+i*dy),size=btn_size,name=key)ifkeyin['0','1','2','3','4','5','6','7','8','9','.']:(1)定义按键的背景色elifkeyin['(',')','Back','Clear']:(2)((217,220,235))((224,60,60))elifkeyin['+','-','*','/']:(2)((246,225,208))((60,60,224))else:(2)((245,227,129))((60,60,224))(u"显示计算结果")(_BUTTON,_button)获取事件对象(哪个按钮被按)key=()播放按键对应频率的声音=='Error':('')ifkey=='Clear':按下了回退键,去掉最后一个输入字符content=()ifcontent:(content[:-1])elifkey=='=':按下了其他键,追加到显示屏上(key)defPlayKeySound(self,key,Dur=100):"""播放按键声音"""([key],Dur)
if__name__=='__main__':app=()frame=MainFrame()()()
代码运行界面如下图所示。

6.3.定时器和线程
在一个桌面程序中,GUI线程是主线程,其他线程若要更新显示内容,Tkinter使用的是类型对象,PyQt使用的信号和槽机制,wxPython则相对原始:它允许子线程更新GUI,但需要借助于()函数。
这个例子里面设计了一个数字式钟表,一个秒表,秒表显示精度十分之一毫秒。从代码设计上来说没有任何难度,实现的方法有很多种,可想要达到一个较好的显示效果,却不是一件容易的事情。请注意体会()的使用条件。
importwximporttimeimportthreading
classMainFrame():"""桌面程序主窗口类"""def__init__(self):"""构造函数"""__init__(self,parent=None,style=|_MENU|_BOX|_BOX|_BORDER)('定时器和线程')(('res/',_TYPE_ICO))((224,224,224))((320,300))self._init_ui()()def_init_ui(self):"""初始化界面"""font=(30,,_NORMAL,_NORMAL,False,'Monaco')=(self,-1,'08:00:00',pos=(50,50),size=(200,50),style=_CENTER|_BORDER)((0,224,32))((0,0,0))(font)=(self,-1,'0:00:00.00',pos=(50,150),size=(200,50),style=_CENTER|_BORDER)((0,224,32))((0,0,0))(font)=(self)(_TIMER,_timer,)(50)(_KEY_DOWN,_key_down)_last=None_start=False_start=Nonethread_sw=(target=)thread_(True)thread_()defon_timer(self,evt):"""定时器函数"""t=()_sec!=_last:('%02d:%02d:%02d'%(_hour,_min,_sec))_last=_secdefon_key_down(self,evt):"""键盘事件函数"""()==_SPACE:_start=_start_start=()()==_ESCAPE:_start=False('0:00:00.00')defStopWatchThread(self):"""线程函数"""whileTrue:_start:t=()-_startti=int(t)(,'%d:%02d:%02d.%.02d'%(ti//3600,ti//60,ti%60,int((t-ti)*100)))(0.02)
if__name__=='__main__':app=()frame=MainFrame()()()
代码运行界面如下图所示。界面上方的时钟一直再跑,下方的秒表则是按键启动或停止。

6.4.DC绘图
DC是DeviceContext的缩写,字面意思是设备上下文——我一直不能正确理解DC这个中文名字,也找不到更合适的说法,所以,我坚持使用DC而不是设备上下文。DC可以在屏幕上绘制点线面,当然也可以绘制文本和图像。事实上,在底层所有控件都是以位图形式绘制在屏幕上的,这意味着,我们一旦掌握了DC这个工具,就可以自己创造我们想要的控件了
DC有很多种,PaintDC,ClientDC,MemoryDC等。通常,我们可以使用ClientDC和MemoryDC,PaintDC是发生重绘事件(_PAINT)时系统使用的。使用ClientDC绘图时,需要记录绘制的每一步工作,不然,系统重绘时会令我们前功尽弃——这是使用DC最容易犯的错误。
importwx
classMainFrame():"""桌面程序主窗口类"""def__init__(self):"""构造函数"""__init__(self,parent=None,style=|_MENU|_BOX|_BOX|_BORDER)('使用DC绘图')(('res/',_TYPE_ICO))((224,224,224))((800,480))self._init_ui()()def_init_ui(self):"""初始化界面"""=(self,-1,style=_BORDER)((0,0,0))btn_base=(self,-1,'文字和图片',size=(100,-1))sizer_max=()sizer_(,1,|||,5)sizer_(btn_base,0,,20)(True)(sizer_max)()btn_(_BUTTON,_base)(_MOUSE_EVENTS,_mouse)(_PAINT,_paint)=None=list()=('res/',_TYPE_ANY)_palette()defon_mouse(self,evt):"""移动鼠标画线"""==10030:左键弹起=None==10036:设置窗口背景色((800,600))self._init_ui()()def_init_ui(self):"""初始化界面"""=Figure()=back_(self,-1,)btn_1=(self,-1,'散点图',size=(80,30))btn_2=(self,-1,'等值线图',size=(80,30))btn_1.Bind(_BUTTON,_scatter)btn_2.Bind(_BUTTON,_contour)sizer_btn=()sizer_(btn_1,0,,20)sizer_(btn_2,0,,20)sizer_max=()sizer_(,1,|,10)sizer_(sizer_btn,0,_CENTER|,20)(sizer_max)()defon_scatter(self,evt):"""散点图"""x=(50)随机生成50个符合标准正态分布的点(y坐标)color=10*(50)随机数表示点的面积()ax=_subplot(111)(x,y,c=color,s=area,cmap='hsv',marker='o',edgecolor='r',alpha=0.5)()defon_contour(self,evt):"""等值线图"""y,x=[-3:3:60j,-4:4:80j]z=(1-y**5+x**5)*(-x**2-y**2)()ax=_subplot(111)_title('有填充的等值线图')c=(x,y,z,levels=8,cmap='jet')(c,ax=ax)()
if__name__=='__main__':app=()frame=MainFrame(None)()()
代码运行界面如下图所示。

7.2.集成OpenGL
是wxPython为显示OpenGL提供的类,顾名思义,可以将其理解为OpenGL的画板。有了这个画板,我们就可以使用OpenGL提供的各种工具在上面绘制各种三维模型了。下面的代码仅是一个demo,并未构建投影系统和视点系统。
importnumpyasnp*
importwxfromwximportglcanvas
classMainFrame():"""从派生主窗口类"""def__init__(self,parent):"""构造函数"""__init__(self,parent,style=_FRAME_STYLE)=(self,style=_GL_RGBA|_GL_DOUBLEBUFFER|_GL_DEPTH_SIZE)=()=()('集成OpenGL')(('res/'))((224,224,224))设置画布背景色()defon_resize(self,evt):"""窗口改变事件函数"""()=()()()defdraw(self):"""绘制"""---------------------------------------------------------------glBegin(GL_LINES)以红色绘制x轴glColor4f(1.0,0.0,0.0,1.0)设置x轴顶点(x轴负方向)glVertex3f(0.8,0.0,0.0)以绿色绘制y轴glColor4f(0.0,1.0,0.0,1.0)设置y轴顶点(y轴负方向)glVertex3f(0.0,0.8,0.0)以蓝色绘制z轴glColor4f(0.0,0.0,1.0,1.0)设置z轴顶点(z轴负方向)glVertex3f(0.0,0.0,0.8)结束绘制线段开始绘制三角形(z轴负半区)glColor4f(1.0,0.0,0.0,1.0)设置三角形顶点glColor4f(0.0,1.0,0.0,1.0)设置三角形顶点glColor4f(0.0,0.0,1.0,1.0)设置三角形顶点gl()---------------------------------------------------------------glBegin(GL_TRIANGLES)设置当前颜色为红色不透明glVertex3f(-0.5,0.5,0.5)设置当前颜色为绿色不透明glVertex3f(0.5,0.5,0.5)设置当前颜色为蓝色不透明glVertex3f(0.0,-0.366,0.5)结束绘制三角形#交换缓冲区()
if__name__=='__main__':app=()frame=MainFrame(None)()()
代码运行界面如下图所示。


成就一亿技术人
