Python 是一种流行的语言,既可以进行脚本编程,也可以进行面向对象编程。 许多框架为 Python 提供了 GUI(图形用户界面),它们各有优点,无论是简单性、效率还是灵活性。 其中最流行的两个是 wxPython 和 PyQt,但它们如何比较? 更重要的是,你的项目应该选择哪个?
外观和感觉
让我们首先解决大多数用户最先注意到的问题——应用程序的外观。
 wxPython 的独特功能之一是其核心库(用 C++ 编写)是其宿主系统原生小部件的包装器。 当你在 GUI 中为按钮小部件编写代码时,你得到的不是看起来像属于另一个操作系统的东西,也不是仅仅是近似值。 相反,你得到的是与使用原生工具编码时相同的对象。
wxPython 的独特功能之一是其核心库(用 C++ 编写)是其宿主系统原生小部件的包装器。 当你在 GUI 中为按钮小部件编写代码时,你得到的不是看起来像属于另一个操作系统的东西,也不是仅仅是近似值。 相反,你得到的是与使用原生工具编码时相同的对象。
https://open-source.net.cn/sites/default/files/wxbutton.png" title="Linux 上的 Thunar 和 WxPython" typeof="foaf:Image" width="492" height="480">
Linux 上的 Thunar 和 WxPython
 这与 PyQt 不同,PyQt 基于著名的 Qt 工具包。 PyQt 也用 C++ 编写,但它不使用原生小部件,而是根据检测到的操作系统创建小部件的近似值。 它做出了很好的近似,我从未遇到过用户——即使在艺术学校,用户往往对外观非常挑剔——抱怨应用程序看起来和感觉不像原生的。
这与 PyQt 不同,PyQt 基于著名的 Qt 工具包。 PyQt 也用 C++ 编写,但它不使用原生小部件,而是根据检测到的操作系统创建小部件的近似值。 它做出了很好的近似,我从未遇到过用户——即使在艺术学校,用户往往对外观非常挑剔——抱怨应用程序看起来和感觉不像原生的。
如果你正在使用 KDE,你可以使用额外的 PyKDE 库来弥合原始 PyQt 和你在 Linux 和 BSD 上的 Plasma 桌面外观之间的差距,但这会增加新的依赖项。
https://open-source.net.cn/sites/default/files/qtbutton.png" title="Linux 上的 KDE 和 Qt" typeof="foaf:Image" width="520" height="288">
Linux 上的 KDE 和 Qt
跨平台
wxPython 和 PyQt 都支持 Linux、Windows 和 Mac,因此它们非常适合著名的跨平台 Python; 但是,不要被“跨平台”这个术语所迷惑——你仍然必须在你的 Python 代码中进行平台特定的调整。 你的 GUI 工具包无法调整到数据目录的路径格式,因此你仍然必须在 Python 中实践最佳实践,使用 os.path.join 和一些不同的 exit 方法等等。 你选择的 GUI 工具包不会神奇地从一个平台抽象到另一个平台。
 PyQt 努力保护你免受跨平台差异的影响。 考虑到 Python 本身需要的常见调整,PyQt 将你与大多数跨平台问题隔离开来,以便你的 GUI 代码保持不变,而与操作系统无关。 总会有例外,但 PyQt 处理得非常出色。 这是一种你会欣赏和钦佩的优势。
PyQt 努力保护你免受跨平台差异的影响。 考虑到 Python 本身需要的常见调整,PyQt 将你与大多数跨平台问题隔离开来,以便你的 GUI 代码保持不变,而与操作系统无关。 总会有例外,但 PyQt 处理得非常出色。 这是一种你会欣赏和钦佩的优势。
 在 wxPython 中,你可能需要对你的 GUI 代码进行一些平台特定的更改,具体取决于你正在编程的内容。 例如,为了防止 Microsoft Windows 上某些元素的闪烁,USE_BUFFERED_DC 属性必须设置为 True 以双缓冲图形。 这不是默认设置,即使它可以无条件地为所有平台完成,因此在某些用例中可能存在缺点,但这是你必须为 wxPython 做出的让步的一个很好的例子。
在 wxPython 中,你可能需要对你的 GUI 代码进行一些平台特定的更改,具体取决于你正在编程的内容。 例如,为了防止 Microsoft Windows 上某些元素的闪烁,USE_BUFFERED_DC 属性必须设置为 True 以双缓冲图形。 这不是默认设置,即使它可以无条件地为所有平台完成,因此在某些用例中可能存在缺点,但这是你必须为 wxPython 做出的让步的一个很好的例子。
安装
作为一名开发人员,你可能不介意获取应用程序所需库的安装步骤; 但是,如果你计划分发你的应用程序,那么你需要考虑你的用户必须经历的安装过程才能运行你的应用程序。
 在任何平台上安装 Qt 都像安装任何其他应用程序一样简单。 给你的用户一个下载链接,告诉他们安装下载的包,他们很快就可以使用你的应用程序了。 这在所有受支持的平台上都是如此。
在任何平台上安装 Qt 都像安装任何其他应用程序一样简单。 给你的用户一个下载链接,告诉他们安装下载的包,他们很快就可以使用你的应用程序了。 这在所有受支持的平台上都是如此。
然而,对于所有平台来说,同样真实的是 PyQt 依赖于 Qt 本身的 C++ 代码。 这意味着用户不仅必须安装 PyQt,还必须安装整个 Qt。 这不是一个小包,并且需要大量的点击,并且可能需要逐步完成安装向导。 然而,Qt 和 PyQt 团队尽可能地简化了安装,因此,尽管这似乎对用户要求很高,但只要你提供直接链接,任何可以安装 Web 浏览器或游戏的用户都应该能够应对 Qt 安装。 如果你非常投入,你甚至可以将安装编写为你自己安装程序的一部分。
在 Linux、BSD 和 Ilumos 家族中,安装通常已经由发行版的软件包管理器为你编写了脚本。
 wxPython 的安装过程在 Linux 和 Windows 上一样简单,但在 Mac OS 上却存在问题。 可下载的软件包严重过时,这是苹果公司对向后兼容性不感兴趣的又一个受害者。 存在一个 bug ticket 包含修复程序,但软件包尚未更新,因此普通用户自己找到并实现补丁的可能性很小。 目前的解决方案是打包 wxPython 并将其分发给你自己的 Mac OS 用户,或者依赖外部软件包管理器(尽管当我上次测试 Mac 的 wxPython 时,即使是那些安装脚本也失败了)。
wxPython 的安装过程在 Linux 和 Windows 上一样简单,但在 Mac OS 上却存在问题。 可下载的软件包严重过时,这是苹果公司对向后兼容性不感兴趣的又一个受害者。 存在一个 bug ticket 包含修复程序,但软件包尚未更新,因此普通用户自己找到并实现补丁的可能性很小。 目前的解决方案是打包 wxPython 并将其分发给你自己的 Mac OS 用户,或者依赖外部软件包管理器(尽管当我上次测试 Mac 的 wxPython 时,即使是那些安装脚本也失败了)。
小部件和功能
PyQt 和 wxPython 都具有你期望从 GUI 工具包获得的所有常用小部件,包括按钮、复选框、下拉菜单等等。 两者都支持拖放操作、选项卡式界面、对话框和自定义小部件的创建。
 PyQt 具有灵活性的优势。 你可以在运行时重新排列、浮动、关闭和恢复 Qt 面板,从而为每个应用程序提供高度可配置的以可用性为中心的界面。
PyQt 具有灵活性的优势。 你可以在运行时重新排列、浮动、关闭和恢复 Qt 面板,从而为每个应用程序提供高度可配置的以可用性为中心的界面。
https://open-source.net.cn/sites/default/files/panelmove.png" title="移动 Qt 面板" typeof="foaf:Image" width="382" height="194">
移动 Qt 面板
只要你使用正确的小部件,这些功能就是内置的,你无需重新发明花哨的技巧来为你的高级用户提供友好的功能。
 WxPython 有许多很棒的功能,但在灵活性和用户控制方面,它无法与 PyQt 相提并论。 一方面,这意味着作为开发人员,设计和布局对你来说更容易。 当在 Qt 上开发时,不需要很长时间,你就会收到来自用户的请求,询问如何跟踪自定义布局,或者如何找到意外关闭的丢失面板等等。 出于同样的原因,wxPython 对你的用户来说更简单,因为当面板首先无法关闭时,丢失意外关闭的面板的踪迹要困难得多。
WxPython 有许多很棒的功能,但在灵活性和用户控制方面,它无法与 PyQt 相提并论。 一方面,这意味着作为开发人员,设计和布局对你来说更容易。 当在 Qt 上开发时,不需要很长时间,你就会收到来自用户的请求,询问如何跟踪自定义布局,或者如何找到意外关闭的丢失面板等等。 出于同样的原因,wxPython 对你的用户来说更简单,因为当面板首先无法关闭时,丢失意外关闭的面板的踪迹要困难得多。
最终,wxPython 毕竟只是 wxWidgets 的前端,因此如果你真的需要某个功能,你也许可以用 C++ 实现它,然后在 wxPython 中使用它。 然而,与 PyQt 相比,这是一个艰巨的任务。
GUI 应用程序由许多较小的视觉元素组成,通常称为“小部件”。 为了使 GUI 应用程序顺利运行,小部件必须相互通信,例如,旨在显示图像的窗格知道用户选择了哪个缩略图。
 大多数 GUI 工具包(包括 wxPython)都使用“回调”处理内部通信。 回调是指向某些代码片段(“函数”)的指针。 例如,如果你想在单击按钮小部件时发生某些事情,你可以为你想要发生的操作编写一个函数。 然后,当单击按钮时,你在代码中调用该函数,操作就会发生。
大多数 GUI 工具包(包括 wxPython)都使用“回调”处理内部通信。 回调是指向某些代码片段(“函数”)的指针。 例如,如果你想在单击按钮小部件时发生某些事情,你可以为你想要发生的操作编写一个函数。 然后,当单击按钮时,你在代码中调用该函数,操作就会发生。
它工作得足够好,只要你将其与 lambda 结合使用,它就是一个非常灵活的解决方案。 有时,根据你希望通信有多么精细,你最终会得到比你预期的更多的代码,但它确实有效。
 另一方面,Qt 以其“信号和槽”机制而闻名。 如果你将 wxPython 的内部通信网络想象成旧式的电话交换机,那么将 PyQt 的通信想象成网状网络。
另一方面,Qt 以其“信号和槽”机制而闻名。 如果你将 wxPython 的内部通信网络想象成旧式的电话交换机,那么将 PyQt 的通信想象成网状网络。
https://open-source.net.cn/sites/default/files/abstract-connections.png" title="Qt 图" typeof="foaf:Image" width="516" height="501">
Qt 中的信号和槽(Qt 图 GFDL 许可)
使用信号和槽,一切都获得一个签名。 发出信号的小部件不需要知道其消息的目标槽,甚至不需要知道它是否注定要到达任何槽。 只要你将信号连接到槽,当信号广播时,就会使用信号的参数调用槽。
槽可以设置为监听任意数量的信号,信号可以设置为广播到任意数量的槽。 你甚至可以将一个信号连接到另一个信号以创建信号的连锁反应。 你永远不必回到你的代码中手动“连接”事物。
信号和槽可以接受任何类型的任意数量的参数。 你不必编写代码来过滤掉你在某些条件下想要或不想要的东西。
更好的是,槽不仅仅是监听器; 它们是正常的函数,无论有没有信号,都可以做有用的事情。 正如对象不知道是否有任何东西在监听它的信号一样,槽也不知道它是否在监听信号。 没有代码块依赖于连接的存在; 如果存在连接,它只是在不同的时间被触发。
无论你是否理解信号和槽,一旦你使用它们,然后尝试回到传统的回调,你就会被迷住。
布局
当你编程一个 GUI 应用程序时,你必须设计它的布局,以便所有的小部件都知道它们在你的应用程序窗口中应该出现在哪里。 就像网页一样,你可能会选择将你的应用程序设计为可调整大小,或者你可能会将其限制为固定大小。 在某些方面,这是 GUI 编程中最 GUI 的部分。
 在 Qt 中,一切都非常合乎逻辑。 小部件被明智地命名(QPushButton、QDial、QCheckbox、QLabel,甚至 QCalendarWidget),并且易于调用。 文档非常出色,只要你经常参考它,并且很容易在其中发现很酷的功能。
在 Qt 中,一切都非常合乎逻辑。 小部件被明智地命名(QPushButton、QDial、QCheckbox、QLabel,甚至 QCalendarWidget),并且易于调用。 文档非常出色,只要你经常参考它,并且很容易在其中发现很酷的功能。
存在潜在的混淆点,主要是在基本级别的 GUI 元素中。 例如,如果你正在编写一个应用程序,你是从 QMainWindow 还是 QWidget 开始来形成你的父窗口? 两者都可以用作你的应用程序的窗口,因此答案是,就像在计算中经常出现的那样:这取决于情况。
QWidget 是一个原始的、空的容器。 它被所有其他小部件使用,但这意味着它也可以按原样使用,以形成你放置更多小部件的父窗口。 QMainWindow 像所有其他小部件一样,使用 QWidget,但它添加了许多大多数应用程序需要的便利功能,例如顶部的工具栏、底部的状态栏等。
https://open-source.net.cn/sites/default/files/qmainwindow.png" title="QMainwindow" typeof="foaf:Image" width="542" height="377">
QMainwindow
一个使用 QMainWindow 的小型文本编辑器,代码行数仅 100 多行
#!/usr/bin/env python
# a minimal text editor to demo PyQt5
# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.
import sys
import os
import pickle
from PyQt5 import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class TextEdit(QMainWindow):
def __init__(self):
    super(TextEdit, self).__init__()
    #font = QFont("Courier", 11)
    #self.setFont(font)
    self.filename = False
    self.Ui()
def Ui(self):
    quitApp = QAction(QIcon('/usr/share/icons/breeze-dark/actions/32/application-exit.svg'), 'Quit', self)
    saveFile = QAction(QIcon('/usr/share/icons/breeze-dark/actions/32/document-save.svg'), 'Save', self)
    newFile = QAction('New', self)
    openFile = QAction('Open', self)
    copyText = QAction('Copy', self)
    pasteText = QAction('Yank', self)
    newFile.setShortcut('Ctrl+N')
    newFile.triggered.connect(self.newFile)
    openFile.setShortcut('Ctrl+O')
    openFile.triggered.connect(self.openFile)
    saveFile.setShortcut('Ctrl+S')
    saveFile.triggered.connect(self.saveFile)
    quitApp.setShortcut('Ctrl+Q')
    quitApp.triggered.connect(self.close)
    copyText.setShortcut('Ctrl+K')
    copyText.triggered.connect(self.copyFunc)
    pasteText.setShortcut('Ctrl+Y')
    pasteText.triggered.connect(self.pasteFunc)
    menubar = self.menuBar()
    menubar.setNativeMenuBar(True)
    menuFile = menubar.addMenu('&File')
    menuFile.addAction(newFile)
    menuFile.addAction(openFile)
    menuFile.addAction(saveFile)
    menuFile.addAction(quitApp)
    menuEdit = menubar.addMenu('&Edit')
    menuEdit.addAction(copyText)
    menuEdit.addAction(pasteText)
    toolbar = self.addToolBar('Toolbar')
    toolbar.addAction(quitApp)
    toolbar.addAction(saveFile)
    self.text = QTextEdit(self)
    self.setCentralWidget(self.text)
    self.setMenuWidget(menubar)
    self.setMenuBar(menubar)
    self.setGeometry(200,200,480,320)
    self.setWindowTitle('TextEdit')
    self.show()
def copyFunc(self):
    self.text.copy()
def pasteFunc(self):
    self.text.paste()
def unSaved(self):
    destroy = self.text.document().isModified()
    print(destroy)
    if destroy == False:
	return False
    else:
	detour = QMessageBox.question(self,
			"Hold your horses.",
			"File has unsaved changes. Save now?",
			QMessageBox.Yes|QMessageBox.No|
			QMessageBox.Cancel)
	if detour == QMessageBox.Cancel:
	    return True
	elif detour == QMessageBox.No:
	    return False
	elif detour == QMessageBox.Yes:
	    return self.saveFile()
    return True
def saveFile(self):
    self.filename = QFileDialog.getSaveFileName(self, 'Save File', os.path.expanduser('~'))
    f = self.filename[0]
    with open(f, "w") as CurrentFile:
	CurrentFile.write(self.text.toPlainText() )
    CurrentFile.close()
def newFile(self):
    if not self.unSaved():
	self.text.clear()
def openFile(self):
    filename, _ = QFileDialog.getOpenFileName(self, "Open File", '', "All Files (*)")
    try:
	self.text.setText(open(filename).read())
    except:
	True
def closeEvent(self, event):
    if self.unSaved():
	event.ignore()
    else:
	exit
def main():
app = QApplication(sys.argv)
editor = TextEdit()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
 wxPython 中的基础小部件是 wx.Window。wxPython 中的一切,无论是实际窗口还是按钮、复选框或文本标签,都基于 wx.Window 类。如果有一个“最名不副实类名”奖项,wx.Window 会被忽略,因为它名字太糟糕了,没人会怀疑它有错。有人告诉我,习惯 wx.Window 不是窗口需要数年时间,这肯定是实话,因为我每次用它都会犯这个错误。
wxPython 中的基础小部件是 wx.Window。wxPython 中的一切,无论是实际窗口还是按钮、复选框或文本标签,都基于 wx.Window 类。如果有一个“最名不副实类名”奖项,wx.Window 会被忽略,因为它名字太糟糕了,没人会怀疑它有错。有人告诉我,习惯 wx.Window 不是窗口需要数年时间,这肯定是实话,因为我每次用它都会犯这个错误。
wx.Frame 类扮演着传统角色,就像你我所认为的桌面窗口一样。要使用 wx.Frame 创建一个空窗口
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wx
class Myframe(wx.Frame):
def __init__(self, parent, title):
    super(Myframe, self).__init__(parent, title=title,
				  size=(520, 340))
    self.Centre()
    self.Show()
if __name__ == '__main__':
app = wx.App()
Myframe(None, title='Just an empty frame')
	app.MainLoop()将其他小部件放在 wx.Frame 窗口内,然后你就构建了一个 GUI 应用程序。例如,wx.Panel 小部件类似于 HTML 中的 div,具有绝对尺寸约束,因此你可以使用它在主窗口中创建面板(但它不是窗口,而是 wx.Frame)。
与 PyQt 相比,WxPython 的便利功能较少。例如,复制和粘贴功能内置于 PyQt 中,而在 wxPython 中必须手动编码(并且仍然部分取决于它运行的平台)。其中一些功能可以通过具有内置功能的优秀桌面系统优雅地处理,但为了与 PyQt 应用程序实现功能对等,wxPython 需要更多手动工作。
https://open-source.net.cn/sites/default/files/wxframe.png" title="wx.Frame" typeof="foaf:Image" width="544" height="397">
wx.Frame
wxPython 中的一个简单文本编辑器
#!/usr/bin/env python
# a minimal text editor to demo wxPython
# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.
import wx
import os
class TextEdit(wx.Frame):
def __init__(self,parent,title):
    wx.Frame.__init__(self,parent,wx.ID_ANY, title, size=(520, 340))
    menuBar  = wx.MenuBar()
    menuFile = wx.Menu()
    menuBar.Append(menuFile,"&File")
    menuFile.Append(1,"&Open")
    menuFile.Append(2,"&Save")
    menuFile.Append(3,"&Quit")
    self.SetMenuBar(menuBar)
    wx.EVT_MENU(self,1,self.openAction)
    wx.EVT_MENU(self,2,self.saveAction)
    wx.EVT_MENU(self,3,self.quitAction)
    self.p1 = wx.Panel(self)        
    self.initUI()
def initUI(self):
    self.text = wx.TextCtrl(self.p1,style=wx.TE_MULTILINE)
    vbox = wx.BoxSizer(wx.VERTICAL )
    vbox.Add( self.p1, 1, wx.EXPAND | wx.ALIGN_CENTER )
    self.SetSizer(vbox)
    self.Bind(wx.EVT_SIZE, self._onSize)
    self.Show()
def _onSize(self, e):
    e.Skip()
    self.text.SetSize(self.GetClientSizeTuple())
def quitAction(self,e):
    if self.text.IsModified():
	dlg = wx.MessageDialog(self,"Quit? All changes will be lost.","",wx.YES_NO)
	if dlg.ShowModal() == wx.ID_YES:
	    self.Close(True)
	else:
	    self.saveAction(self)
    else:
	exit()
def openAction(self,e):
    dlg = wx.FileDialog(self, "File chooser", os.path.expanduser('~'), "", "*.*", wx.OPEN)
    if dlg.ShowModal() == wx.ID_OK:
	filename = dlg.GetFilename()
	dir = dlg.GetDirectory()
	f = open(os.path.join(dir, filename),'r')
	self.text.SetValue(f.read())
	f.close()
    dlg.Destroy()
def saveAction(self,e):
    dlg = wx.FileDialog(self, "Save as", os.path.expanduser('~'), "", "*.*", wx.SAVE | wx.OVERWRITE_PROMPT)
    if dlg.ShowModal() == wx.ID_OK:
	filedata = self.text.GetValue()
	filename = dlg.GetFilename()
	dir = dlg.GetDirectory()
	f = open(os.path.join(dir, filename),'w')
	f.write(filedata)
	f.close()
    dlg.Destroy()
def main():
app = wx.App(False)
view = TextEdit(None, "TextEdit")
app.MainLoop()
if __name__ == '__main__':
main()你应该使用哪一个?
PyQt 和 wxPython GUI 工具包各有优势。
 WxPython 大多很简单,当它不简单时,对于不害怕拼凑解决方案的 Python 程序员来说,它也很直观。你不会发现很多必须被灌输的“wxWidget 方式”的实例。它是一个工具包,包含你可以用来拼凑 GUI 的各种组件。如果你的目标用户群已知已安装 GTK,那么 wxPython 可以利用 GTK,依赖性极小。
WxPython 大多很简单,当它不简单时,对于不害怕拼凑解决方案的 Python 程序员来说,它也很直观。你不会发现很多必须被灌输的“wxWidget 方式”的实例。它是一个工具包,包含你可以用来拼凑 GUI 的各种组件。如果你的目标用户群已知已安装 GTK,那么 wxPython 可以利用 GTK,依赖性极小。
额外的好处是,它使用原生小部件,因此你的应用程序看起来应该与目标计算机上预装的应用程序没有区别。
不过,不要太在意 wxPython 声称的跨平台性。它有时在某些平台上存在安装问题,并且没有那么多抽象层来屏蔽平台之间的差异。
 PyQt 功能强大,几乎总是需要安装一些依赖项(尤其是在非 Linux 和非 BSD 目标平台上)。伴随所有这些庞大的代码而来的是大量的便利性。Qt 尽最大努力屏蔽平台之间的差异;它为你提供了惊人数量的预构建函数、小部件和抽象。它得到了很好的支持,许多公司都依赖它作为其基础框架,并且一些最重要的开源项目都在使用它并为其做出贡献。
PyQt 功能强大,几乎总是需要安装一些依赖项(尤其是在非 Linux 和非 BSD 目标平台上)。伴随所有这些庞大的代码而来的是大量的便利性。Qt 尽最大努力屏蔽平台之间的差异;它为你提供了惊人数量的预构建函数、小部件和抽象。它得到了很好的支持,许多公司都依赖它作为其基础框架,并且一些最重要的开源项目都在使用它并为其做出贡献。
如果你刚开始,你应该尝试一下每一种,看看哪一种吸引你。如果你是一位经验丰富的程序员,尝试一下你还没用过的,看看你的想法。两者都是开源的,所以你不必只选择一个。重要的是要知道何时使用哪种解决方案。
祝你编程愉快。
 
 
 
 
 

13 条评论