wxPython:一曲MFC的挽歌,理想主义的绝唱

Posted 天元浪子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了wxPython:一曲MFC的挽歌,理想主义的绝唱相关的知识,希望对你有一定的参考价值。

文章目录


1. 概述

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通常要这样写:

from PyQt6.QtWidgets import QApplication, QWidget, QComboBox, QPushButton, QHBoxLayout, QVBoxLayout, QColorDialog
from PyQt6.QtGui import QIcon, QPainter, QPen, QColor, QPolygon
from PyQt6.QtCore import Qt, QPoint

PyQt巨人般的体量限制了使用星号导入所有的模块,只能用什么导入什么。而wxPython只需要简短的一句话:

import wx

再比如一些常量的写法,wxPython同样简洁,PyQt已经长到匪夷所思的程度了。比如左对齐和确定取消键,wxPython这样写:

wx.ALIGN_LEFT
wx.OK | wx.CANCEL

PyQt写出来几乎要占一整行:

Qt.AlignmentFlag.AlignLeft
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel

尽管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用户可以直接使用下面的命令安装。

pip install -U wxPython

由于Linux平台存在发行版之间的差异,必须使用相应的包管理器进行下载和安装。 例如,在Ubuntu系统上可以尝试下面的安装命令。

sudo apt-get install python3-wxgtk4.0 python3-wxgtk-webview4.0 python3-wxgtk-media4.0

2 快速体验

2.1 桌面应用程序开发的一般流程

用wxPython写一个桌面应用程序,通常分为6个步骤:

  • 第1步:导入模块
  • 第2步:创建一个应用程序
  • 第3步:创建主窗口
  • 第4步:在主窗口上实现业务逻辑
  • 第5步:显示窗主口
  • 第6步:应用程序进入事件处理主循环

除第4步之外的其它步骤,基本都是一行代码就可以完成,第4步的复杂程度取决于功能需求的多寡和业务逻辑的复杂度。下面这段代码就是这个一般流程的体现。

# 第1步:导入模块
import wx 

# 第2步:创建一个应用程序
app = wx.App() 

# 第3步:创建主窗口
frame = wx.Frame(None) 

# 第4步:在主窗口上实现业务逻辑
st = wx.StaticText(frame, -1, 'Hello World') 

# 第5步:显示窗主口
frame.Show() 

# 第6步:应用程序进入事件处理主循环
app.MainLoop()

2.2 Hello World

实际应用wxPython开发桌面应用程序的的时候,上面这样的写法难以实现和管控复杂的业务逻辑,因而都是采用面向对象的应用方式。下面的代码演示了以OOP的方式使用wxPython,并且为窗口增加了标题和图标,设置了窗口尺寸和背景色,同时也给静态文本控件StaticText设置了字体字号。

import wx

class MainFrame(wx.Frame):
    """从wx.Frame派生主窗口类"""
    
    def __init__(self, parent):
        """构造函数"""
        
        wx.Frame.__init__(self, parent, -1,style=wx.DEFAULT_FRAME_STYLE)
        
        self.SetTitle('最简的的应用程序')
        self.SetIcon(wx.Icon('res/wx.ico')) # 设置图标
        self.SetBackgroundColour((217, 228, 0)) # 设置窗口背景色
        self.SetSize((300, 80)) # 设置窗口大小
        self.Center() # 窗口在屏幕上居中
        
        st = wx.StaticText(self, -1, 'Hello World', style=wx.ALIGN_CENTER) # 生成静态文本控件,水平居中
        st.SetFont(wx.Font(20, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False, 'Arial')) # 设置字体字号

if __name__ == '__main__':
    app = wx.App() # 创建一个应用程序
    frame = MainFrame(None) # 创建主窗口
    frame.Show() # 显示窗主口
    app.MainLoop() # 应用程序进入事件处理主循环

代码中用到了一个.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)等三种不同的重载形式。方法重载固然带来了很多便利,但也会增加使用难度,让用户无所适从。

下面的代码演示了上述常用控件的使用方法。

import wx

class MainFrame(wx.Frame):
    """从wx.Frame派生主窗口类"""
    
    def __init__(self, parent):
        """构造函数"""
        
        # 调用父类的构造函数,从默认风格中去除改变窗口大小
        wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
        
        self.SetTitle('wxPython控件演示')
        self.SetIcon(wx.Icon('res/wx.ico'))
        self.SetSize((860, 450))
        self.Center()
        
        # 创建一个面板,用于放置控件
        panel = wx.Panel(self, -1)

        # 在x=20,y=20的位置,创建静态文本控件
        st = wx.StaticText(panel, -1, '我是静态文本控件', pos=(20, 20))

        # 在x=300,y=20的位置,创建静态图片
        bmp = wx.Bitmap('res/forever.png')
        sb = wx.StaticBitmap(panel, -1, bmp, pos=(280, 10))

        # 在x=20, y=50的位置,创建文本输入框,指定输入框的宽度为260像素,高度默认
        tc1 = wx.TextCtrl(panel, -1, value='我是文本输入框', pos=(20, 50), size=(260, -1))

        # 在x=20, y=90的位置,创建文本输入框,指定样式为密码
        tc2 = wx.TextCtrl(panel, -1, value='我是密码', pos=(20, 90), style=wx.TE_PASSWORD)

        # 在x=20, y=130的位置,创建单选按钮,成组的单选按钮,第一个需要指定样式wx.RB_GROUP
        rb1 = wx.RadioButton(panel, -1, '单选按钮1', pos=(20, 130), style=wx.RB_GROUP, name='rb1')

        # 在x=100, y=130的位置,创建单选按钮,不再需要指定样式wx.RB_GROUP
        rb2 = wx.RadioButton(panel, -1, '单选按钮2', pos=(100, 130), name='rb2')

        # 在x=180, y=130的位置,创建单选按钮,不再需要指定样式wx.RB_GROUP
        rb3 = wx.RadioButton(panel, -1, '单选按钮3', pos=(180, 130), name='rb3')

        # 在x=20, y=160的位置,创建复选按钮
        cb1 = wx.CheckBox(panel, -1, '复选按钮', pos=(20, 160))

        # 在x=100, y=160的位置,创建复选按钮,指定其样式为wx.ALIGN_RIGHT
        cb2 = wx.CheckBox(panel, -1, '文字在左侧的复选按钮', pos=(100, 160), style=wx.ALIGN_RIGHT)

        # 在x=20,y=190的位置,创建按钮
        ch = wx.Choice(panel, -1, choices=['wxPython', 'PyQt', 'Tkinter'], pos=(20, 190), size=(100, -1))
        ch.SetSelection(0)
        
        # 在x=120,y=190的位置,创建按钮
        btn = wx.Button(panel, -1, '按钮', pos=(150, 190))

        # 在x=20,y=230的位置,创建文本框,指定大小为260*150,并指定其样式为多行和只读
        tc3 = wx.TextCtrl(panel, -1, value='我是多行文本输入框', pos=(20, 230), size=(260, 150), style=wx.TE_MULTILINE | wx.CB_READONLY)

if __name__ == '__main__':
    app = wx.App() # 创建一个应用程序
    frame = MainFrame(None) # 创建主窗口
    frame.Show() # 显示窗主口
    app.MainLoop() # 应用程序进入事件处理主循环

代码运行界面如下图所示。

3. 控件布局

3.1. 分区布局

上面的例子里,输入框、按钮等控件的位置由其pos参数确定,即绝对定位。绝对定位这种布局方式非常直观,但不能自动适应窗口的大小变化。更普遍的方式是使用被称为布局管理器的wx.Sizer来实现分区布局。所谓分区布局,就是将一个矩形区域沿水平或垂直方向分割成多个矩形区域,并可嵌套分区布局管理器wx.Sizer的派生类有很多种,最常用到是wx.BoxSizer和wx.StaticBoxSizer。

和一般的控件不同,布局管理器就像是一个魔法口袋:它是无形的,但可以装进不限数量的任意种类的控件——包括其他的布局管理器。当然,魔法口袋也不是万能的,它有一个限制条件:装到里面的东西,要么是水平排列的,要么是垂直排列的,不能排成方阵。好在程序员可以不受限制地使用魔法口袋,当我们需要排成方阵时,可以先每一行使用一个魔法口袋,然后再把所有的行装到一个魔法口袋中。

创建一个魔法口袋,装进几样东西,然后在窗口中显示的伪代码是这样的:

魔法口袋 = wx.BoxSizer() # 默认是水平的,想要垂直放东西,需要加上 wx.VERTICAL 这个参数
魔法口袋.add(确认按钮, 0, wx.ALL, 0) # 装入确认按钮
魔法口袋.add(取消按钮, 0, wx.ALL, 0) # 装入取消按钮

窗口.SetSizer(魔法口袋) # 把魔法口袋放到窗口上
窗口.Layout() # 窗口重新布局

魔法口袋的 add() 方法总共有4个参数:第1个参数很容易理解,就是要装进口袋的物品;第2个参数和所有 add() 方法的第2个参数之和的比,表示装进口袋的物品占用空间的比例,0表示物品多大就占多大地儿,不额外占用空间;第3个参数相对复杂些,除了约定装进口袋的物品在其占用的空间里面水平垂直方向的对齐方式外,还可以指定上下左右四个方向中的一个或多个方向的留白(padding);第4个参数就是留白像素数。

下面是一个完整的例子。

import wx

class MainFrame(wx.Frame):
    """从wx.Frame派生主窗口类"""
    
    def __init__(self, parent):
        """构造函数"""
        
        wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
        
        self.SetTitle('分区布局')
        self.SetIcon(wx.Icon('res/wx.ico'))
        self.SetSize((640, 320)) # 设置窗口大小
        
        self._init_ui() # 初始化界面
        self.Center() # 窗口在屏幕上居中
    
    def _init_ui(self):
        """初始化界面"""
        
        # 创建容器面板
        panel = wx.Panel(self, -1)
        
        # 生成黑色背景的预览面板
        view = wx.Panel(panel, -1, style=wx.SUNKEN_BORDER)
        view.SetBackgroundColour(wx.Colour(0, 0, 0))
        
        # 生成按钮和多行文本控件
        btn_capture = wx.Button(panel, -1, '拍照', size=(100, -1))
        btn_up = wx.Button(panel, -1, '↑', size=(30, 30))
        btn_down = wx.Button(panel, -1, '↓', size=(30, 30))
        btn_left = wx.Button(panel, -1, '←', size=(30, 30))
        btn_right = wx.Button(panel, -1, '→', size=(30, 30))
        tc = wx.TextCtrl(panel, -1, '', style=wx.TE_MULTILINE)
        
        # 左右按钮装入一个水平布局管理器
        sizer_arrow_mid = wx.BoxSizer()
        sizer_arrow_mid.Add(btn_left, 0, wx.RIGHT, 16)
        sizer_arrow_mid.Add(btn_right, 0, wx.LEFT, 16)
        
        # 生成带标签的垂直布局管理器
        sizer_arrow = wx.StaticBoxSizer(wx.StaticBox(panel, -1, '方向键'), wx.VERTICAL)
        sizer_arrow.Add(btn_up, 0, wx.ALIGN_CENTER|wx.ALL, 0) # 装入上按钮
        sizer_arrow.Add(sizer_arrow_mid, 0, wx.TOP|wx.BOTTOM, 1) # 装入左右按钮
        sizer_arrow.Add(btn_down, 0, wx.ALIGN_CENTER|wx.ALL, 0) # 装入下按钮
        
        # 生成垂直布局管理器
        sizer_right = wx.BoxSizer(wx.VERTICAL)
        sizer_right.Add(btn_capture, 0, wx.ALL, 20) # 装入拍照按钮
        sizer_right.Add(sizer_arrow, 0, wx.ALIGN_CENTER|wx.ALL, 0) # 装入方向键
        sizer_right.Add(tc, 1, wx.ALL, 10) # 装入多行文本控件
        
        # 生成水平布局管理器
        sizer_max = wx.BoxSizer()
        sizer_max.Add(view, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.BOTTOM, 5) # 装入左侧的预览面板
        sizer_max.Add(sizer_right, 0, wx.EXPAND|wx.ALL, 0) # 装入右侧的操作区
        
        # 为容器面板指定布局管理器,并调用布局方法完成界面布局
        panel.SetSizer(sizer_max)
        panel.Layout()

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

代码运行界面如下图所示。

3.2. 栅格布局

顾名思义,栅格布局就是将布局空间划分成网格,将控件放置到不同的网格内。栅格布局比较简单,用起来非常方便。栅格布局布局管理器也有很多种,GridBagSizer是最常用的一种。下面是一个使用GridBagSizer实现栅格布局的例子。

import wx

class MainFrame(wx.Frame):
    """从wx.Frame派生主窗口类"""
    
    def __init__(self, parent):
        """构造函数"""
        
        wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
        
        self.SetTitle('栅格布局')
        self.SetIcon(wx.Icon('res/wx.ico'))
        self.SetSize((800, 440)) # 设置窗口大小
        
        self._init_ui() # 初始化界面
        self.Center() # 窗口在屏幕上居中
    
    def _init_ui(self):
        """初始化界面"""
        
        panel = wx.Panel(self, -1) # 创建容器面板
        sizer = wx.GridBagSizer(10, 10)# 每个控件之间横纵间隔10像素
        
        st = wx.StaticText(panel, -1, "用户名")
        sizer.Add(st, (0, 0), flag=wx.TOP | wx.ALIGN_RIGHT, border=20) # 在第0行0列,距离上边缘20像素,右对齐

        userName = wx.TextCtrl(panel, -1)
        sizer.Add(userName, (0, 1), (1, 3), flag=wx.EXPAND | wx.TOP, border=20) # 在第0行1列,跨3列,距离上边缘20像素

        sb = wx.StaticBitmap(panel, -1, wx.Bitmap('res/python.jpg'))
        sizer.Add(sb, (0, 5), (7, 1), flag=wx.TOP | wx.RIGHT, border=20) # 在第0行4列,跨7行,距离上右边缘20像素

        st = wx.StaticText(panel, -1, "密码")
        sizer.Add(st, (1, 0), flag=wx.ALIGN_RIGHT) # 在第1行0列,右对齐

        password = wx.TextCtrl(panel, -1, style=wx.TE_PASSWORD)
        sizer.Add(password, (1, 1), (1, 3), flag=wx.EXPAND) # 在第1行1列,跨3列

        st = wx.StaticText(panel, -1, "学历")
        sizer.Add(st, (2, 无心剑中译雪莱诗14首

从学习python到用wxpython编写接口和客户端

如何从 wxPython 应用程序中捕获所有异常?

如何从 wxPython 中的文本控件传递突出显示的文本

为我的外婆和奶奶写上我的挽歌

《房思琪的初恋乐园》向死而生的文学绝唱