Python 是一种流行的语言,能够进行脚本编写以及面向对象编程。一些框架为 Python 提供了 GUI(图形用户界面),其中大多数框架在某些方面都表现出色,无论是简单性、效率还是灵活性。其中最流行的两个是 wxPython 和 PyQt,但它们之间有何区别?更重要的是,你的项目应该选择哪个?
外观和感觉
让我们首先解决大多数用户首先注意到的问题——应用程序的外观。
wxPython 的独特功能之一是其核心库(用 C++ 编写)是围绕其宿主系统的原生小部件的包装器。当你在 GUI 中为按钮小部件编写代码时,你不会得到看起来像属于另一个操作系统的东西,也不会得到仅仅是近似值的东西。相反,你得到的是与使用原生工具编码时相同的对象。
https://open-source.net.cn/sites/default/files/wxbutton.png" title="Thunar and WxPython on Linux" typeof="foaf:Image" width="492" height="480">
Linux 上的 Thunar 和 WxPython
这与 PyQt 不同,PyQt 基于著名的 Qt 工具包。PyQt 也是用 C++ 编写的,但它不使用原生小部件,而是根据它检测到的操作系统创建小部件的近似值。它做出了很好的近似,我从未遇到过用户——即使是在艺术学校,用户往往对外观吹毛求疵——抱怨应用程序看起来和感觉不原生。
如果你使用 KDE,你还有额外的 PyKDE 库可供你使用,以弥合原始 PyQt 与 Linux 和 BSD 上 Plasma 桌面的外观之间的差距,但这会增加新的依赖项。
https://open-source.net.cn/sites/default/files/qtbutton.png" title="KDE and Qt on Linux" 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 处理得非常好。这将是你欣赏和钦佩的一种奢侈。
在 wxPython 中,你可能需要对你的 GUI 代码进行一些特定于平台的更改,具体取决于你正在编程的内容。例如,为了防止 Microsoft Windows 上某些元素的闪烁,必须将 USE_BUFFERED_DC 属性设置为 True 以双缓冲图形。即使可以无条件地为所有平台执行此操作,但这也不是默认设置,因此在某些用例中可能存在缺点,但这是你必须为 wxPython 做出让步的一个很好的例子。
安装
作为开发人员,你可能不介意获取应用程序所需库的安装步骤;但是,如果你计划分发你的应用程序,那么你需要考虑你的用户必须经历的安装过程才能使你的应用程序运行。
在任何平台上安装 Qt 都像安装任何其他应用程序一样简单:。给你的用户一个下载链接,告诉他们安装下载的软件包,他们很快就能使用你的应用程序了。这在所有支持的平台上都是如此。
然而,对于所有平台来说,PyQt 都依赖于 Qt 本身的 C++ 代码。这意味着用户不仅要安装 PyQt,还要安装所有 Qt。这不是一个小软件包,需要大量的点击,并且可能需要逐步完成安装向导。然而,Qt 和 PyQt 团队使安装尽可能容易,因此,尽管这似乎对用户来说要求很多,但只要你提供直接链接,任何可以安装 Web 浏览器或游戏的用户都应该能够应对 Qt 安装。如果你非常执着,你甚至可以将安装编写为你自己的安装程序的一部分。
在 Linux、BSD 和 Ilumos 系列上,安装通常已经由发行版的软件包管理器为你编写脚本。
wxPython 的安装过程在 Linux 和 Windows 上一样简单,但在 Mac OS 上却存在问题。可下载的软件包严重过时,这是 Apple 对向后兼容性不感兴趣的另一个受害者。错误报告 中存在一个修复程序,但软件包尚未更新,因此普通用户找到并实现补丁的可能性很小。目前的解决方案是将 wxPython 打包并分发给你的 Mac OS 用户,或者依赖外部软件包管理器(尽管当我上次测试 Mac 版 wxPython 时,即使这些安装脚本也失败了)。
小部件和功能
PyQt 和 wxPython 都具有你从 GUI 工具包中期望的所有常用小部件,包括按钮、复选框、下拉菜单等等。两者都支持拖放操作、选项卡式界面、对话框和自定义小部件的创建。
PyQt 具有灵活性的优势。你可以在运行时重新排列、浮动、关闭和恢复 Qt 面板,从而为每个应用程序提供高度可配置的以可用性为中心的界面。
https://open-source.net.cn/sites/default/files/panelmove.png" title="Moving Qt panels" typeof="foaf:Image" width="382" height="194">
移动 Qt 面板
只要你使用正确的小部件,这些功能都是内置的,你无需重新发明花哨的技巧来为你的高级用户提供友好的功能。
WxPython 具有许多强大的功能,但在灵活性和用户控制方面无法与 PyQt 相提并论。一方面,这意味着作为开发人员,设计和布局对你来说更容易。当在 Qt 上进行开发时,很快你就会收到用户的请求,要求提供跟踪自定义布局的方法,或者如何找到意外关闭的丢失面板等等。出于同样的原因,wxPython 对你的用户来说更简单,因为当面板一开始就无法关闭时,丢失意外关闭的面板要困难得多。
毕竟,wxPython 最终只是 wxWidgets 的前端,因此如果你真的需要某个功能,你可能可以用 C++ 实现它,然后在 wxPython 中使用它。然而,与 PyQt 相比,这是一个艰巨的任务。
一个 GUI 应用程序由许多较小的可视化元素组成,通常称为“小部件”。为了使 GUI 应用程序顺利运行,小部件必须相互通信,例如,旨在显示图像的窗格知道用户选择了哪个缩略图。
大多数 GUI 工具包(包括 wxPython)都使用“回调”处理内部通信。回调是指向某段代码(“函数”)的指针。如果你想在例如单击按钮小部件时执行某些操作,你可以为你想要发生的操作编写一个函数。然后,当单击按钮时,你在代码中调用该函数,操作就会发生。
它工作得很好,只要你将其与 lambda 结合使用,它就是一个非常灵活的解决方案。有时,根据你希望通信有多么复杂,你最终会得到比你预期的更多的代码,但它确实有效。
另一方面,Qt 以其“信号和槽”机制而闻名。如果你将 wxPython 的内部通信网络想象成旧式的电话交换机,那么将 PyQt 的通信想象成网状网络。
https://open-source.net.cn/sites/default/files/abstract-connections.png" title="Qt diagram" typeof="foaf:Image" width="516" height="501">
Qt 中的信号和槽(Qt 图 GFDL 许可证)
使用信号和槽,一切都获得了签名。发出信号的小部件不需要知道其消息的目标槽,甚至不需要知道它是否以任何槽为目标。只要你将信号连接到槽,当信号广播时,槽就会使用信号的参数被调用。
槽可以设置为侦听任意数量的信号,信号可以设置为广播到任意数量的槽。你甚至可以将一个信号连接到另一个信号以创建信号的连锁反应。你永远不必回到你的代码中手动“连接”事物。
信号和槽可以接受任意数量的任意类型的参数。你无需编写代码来过滤掉你在特定条件下想要或不想要的东西。
更好的是,槽不仅仅是侦听器;它们是正常的函数,可以执行有用的操作,无论是否有信号。正如对象不知道是否有任何东西在侦听其信号一样,槽也不知道它是否在侦听信号。任何代码块都不会依赖于连接的存在;如果有连接,它只是在不同的时间被触发。
无论你是否理解信号和槽,一旦你使用它们,然后尝试回到传统的回调,你就会被迷住。
布局
当你编程一个 GUI 应用程序时,你必须设计其布局,以便所有小部件都知道在你的应用程序窗口中显示在哪里。就像网页一样,你可以选择将你的应用程序设计为可调整大小,或者你可以将其限制为固定大小。在某些方面,这是 GUI 编程中最 GUI 的部分。
在 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 多行 Python 代码
#!/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 不是窗口需要数年时间,这肯定是事实,因为每次我使用它时都会犯这个错误。
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 可以通过最少的依赖项来利用它。
作为奖励,它使用原生小部件,因此你的应用程序的外观应该与目标计算机上预装的应用程序没有什么不同。
但是,不要太在意 wxPython 声称的跨平台性。它有时在某些平台上存在安装问题,并且它没有那么多抽象层来屏蔽你免受平台之间的差异。
PyQt 很大,并且几乎总是需要安装一些依赖项(尤其是在非 Linux 和非 BSD 目标上)。伴随着所有这些庞大的代码而来的是很多便利性。Qt 尽最大努力屏蔽你免受平台差异的影响;它为你提供了惊人数量的预构建函数、小部件和抽象。它得到了很好的支持,许多公司都依赖它作为其基础框架,并且一些最重要的开源项目使用并为其做出贡献。
如果你刚刚开始,你应该尝试一下每个工具包,看看哪个更吸引你。如果你是一位经验丰富的程序员,请尝试一个你尚未使用的工具包,看看你的想法。两者都是开源的,因此你不必只选择一个。重要的是要知道何时使用哪种解决方案。
祝你编程愉快。
13 条评论