今天和实验室的Juan同学聊起了GUI编程的问题。这家伙最近搞了下C#,并写了个简单的处理GREET软件输入输出的小App。对于我们这种常年接触命令行的家伙,好容易见到个窗口真是分外眼红。接着我忽然就想到了Python做GUI编程的问题,以前一直想搞搞这方面的东西,被这么一提醒,就花了一晚上做了下尝试。
(1)介绍
Python的好处是方便易写,我个人认为,使用Python是拿运行效率换程序猿时间,如果运行效率比程序员时间更重要的话,自然可以考虑用C或者别的语言来编写程序(通常这用来写核心代码)。GUI是程序中属于不怎么对运行效率敏感的部分,因此用拓展性强的Python来写的确很合适。另外,由于目前做的东西的限制,我需要用到许多大量的第三方Python绘图库,而用Python写界面的话能直接把这些第三方库的功能嵌入软件中,这的确是非常有吸引力。
说道这里我又要比Matlab了。大约一个月前用Matlab做过个简单的GUI界面,总体符合我对Matlab的一贯印象,简洁方便。但是受限于Mathworks公司现在提供的组件数目过少,而且无法摆脱Matlab平台依赖性,用着还是觉得不爽
(按我朋友的话来讲,有种太监带着假JB的感觉)。
Python作为通用编程语言,再这方面自然比Matlab要强一些。我粗略查了一下,目前有许多第三方的库都能方便程序员构建窗口。
我大致进行了搜索,希望能找到第三方库拥有一下四个特质:
a. 功能强大且丰富的组件库;
b. 能够实现交互式界面设计(支持控件拖拽);
c. 与py2exe兼容,能直接编译成exe文件;
d. 完全免费(包括商业应用)。
最终找到了PySide和Qt Designer这个组合,经过试用,可以完美实现我的要求。
(2)安装
PySide是由诺基亚公司开发的,简单来说就是其Qt库的Python移植(原先是C++的)。诺基亚还有跨平台的开发工具Qt,完全都是免费使用,在这里不得不赞一下诺基亚。PySide的主要对手是PyQt。PyQt出现得比PySide早,但是PySide血统更纯正一些(毕竟是亲儿子)。但是具体应用两者差不多,因为目前Qt的IDE也没有把Python弄进去,要实现同样功能两者要进行的操作大同小异。PyQt个人应用免费,但是商业应用要收钱,相比下PySide的个人和商业双免费的策略更让人放心一些。
PySide的安装可以直接到官网上下载,这里我就不废话了,有几件事情需要注意:
首先是Pyside不支持Python 2.7 一下版本,为此我终于咬牙从2.5升级了,希望其他库不要出现问题,版本问题真是Python最让人恼火的地方;
其次是PySide似乎还对Python一个组件存在依赖性,但是Python 2.7并没有默认安装这个组件,如果爆出缺少pkg资源一类的错误,需要到官网进行下载,链接:
https://pypi.python.org/pypi/setuptools/0.6c11#downloads
另外,为了方便应用,建议安装完成后把python目录下的script文件夹加到系统的Path中。
现在Qt Designer直接集成到了PySide中了,所以不用去Qt官网下载Qt软件了。500M的东西啊,我坑爹地下完了才发现根本不用下,几年前的教程害死人。Qt Designer在PySide包的文件夹中,具体可以到Python27Libsite-packages中去看,名字叫Designer.exe。
(3)PySide使用
PySide的使用非常方便,下面是一个简单是示例,用纯命令行建立一个标题为Test Form的窗口:
# TestPySide.py
import sys
from PySide.QtCore import *
from PySide.QtGui import *
# Define Form
class Form(QDialog):
def __init__(self,parent=None):
super(Form,self).__init__(parent)
self.setWindowTitle('Test Form')
# Main function
if __name__=='__main__':
# Create Qt App
app=QApplication(sys.argv)
# Create window
NewForm=Form()
NewForm.show()
# exit
sys.exit(app.exec_())
程序先是导入Qt的库,接着继承QDialog类,建立了一个新类Form,标题为“Test Form”,接着在主程序中先是新建app,然后建立NewForm对象,并显示这个对象。
写完后直接运行结果如下:
更具体的示例要自己去参考官方文档了。
(4)使用Qt Designer来实现交互式GUI设计
用纯代码的形式进行界面编写,有个好处是写出来的界面能很容易自适应窗口大小,不过写复杂界面的时候会比较麻烦。为此我们需要一个类似Visual Studio的交互式设计界面。
上面我也说Qt IDE本身并不支持Python,但是Qt IDE中有个Qt Designer的组件,能够进行Qt组件的交互式设计,并且输出成 .ui格式的文件(其实就是xml文件)。PySide的PySide-uic程序(就在Script文件夹下面)能够将这个ui文件转化成为py文件,之后就可以直接用PySide进行调用。
首先打开Python27Libsite-packages下面的Designer.exe,新建一个窗口,拖拽什么的我就不多说了。这个示例我
经过30秒的
精雕细琢,建立了这个日历+按钮界面:
完成后保存成为TestGUI.ui文件。可以先打开看一眼:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>310</width>
<height>284</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>310</width>
<height>284</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>310</width>
<height>284</height>
</size>
</property>
<property name="windowTitle">
<string>Useless Windows</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>80</x>
<y>240</y>
<width>141</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>Useless Button</string>
</property>
</widget>
<widget class="QCalendarWidget" name="calendarWidget">
<property name="geometry">
<rect>
<x>30</x>
<y>40</y>
<width>248</width>
<height>169</height>
</rect>
</property>
</widget>
</widget>
</widget>
<resources/>
<connections/>
</ui>
看着略复杂。接着我们写个bat文件把这个ui文件转换成为py文件:
pyside-uic TestGUI.ui -o TestGUI.py
顺利的话就可以在同一个目录下拿到TestGUI.py文件,接着打开看一眼对比下:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'TestGUI.ui'
#
# Created: Thu Jun 20 18:55:06 2013
# by: pyside-uic 0.2.14 running on PySide 1.1.2
#
# WARNING! All changes made in this file will be lost!
from PySide import QtCore, QtGui
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(310, 284)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
MainWindow.setSizePolicy(sizePolicy)
MainWindow.setMinimumSize(QtCore.QSize(310, 284))
MainWindow.setMaximumSize(QtCore.QSize(310, 284))
self.centralwidget = QtGui.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.pushButton = QtGui.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(80, 240, 141, 23))
self.pushButton.setObjectName("pushButton")
self.calendarWidget = QtGui.QCalendarWidget(self.centralwidget)
self.calendarWidget.setGeometry(QtCore.QRect(30, 40, 248, 169))
self.calendarWidget.setObjectName("calendarWidget")
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Useless Windows", None, QtGui.QApplication.UnicodeUTF8))
self.pushButton.setText(QtGui.QApplication.translate("MainWindow", "Useless Button", None, QtGui.QApplication.UnicodeUTF8))
可以看到,实际上是生成了一个类,这个类包含了窗口的所有信息。
然后就写一个和(3)类似的程序,来调用这个类,并生成新窗口:
# TestProgramWithGUI.py
# This is a program for testing Qt GUI and PySide
import sys
# Import Qt GUI component
from PySide.QtGui import *
# Import GUI File
from TestGUI import Ui_MainWindow
# Self Function
def PrintHello():
print("Hello")
# Make main window class
class MainWindow(QMainWindow,Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow,self).__init__(parent)
self.setupUi(self)
# Connect button click event to PrintHello function
self.pushButton.clicked.connect(PrintHello)
# End of main window class
# Main Function
if __name__=='__main__':
Program = QApplication(sys.argv)
Window=MainWindow()
Window.show()
Program.exec_()
我还给按钮链接了自定义的PrintHello()函数,这样每次按按钮就会在Console输出Hello。为什么称这个为Useless Button呢,因为后来我在py2exe打包的过程中去掉了Console,所以Hello就输出到空气里了。
写完运行,大功告成:
(5)打包成exe文件
接下来就是用py2exe打包成不需要依赖python的exe文件了。前两年的教程还经常写要添加这个添加那个,现在新的PySide已经非常修正了大部分bug了,只要正常编写Setup文件就可以。
我写的文件如下:
from distutils.core import setup
import py2exe
# Set options
options ={ 'py2exe':
{
'dll_excludes':['w9xpopen.exe'] #This file is for win9x platform
}
}
# Setup
setup ( options = options,
windows = [{
'script': 'TestProgramWithGUI.py'
}]
)
由于是窗体文件,setup里用的是windows选项,这就屏蔽了Console。经过满屏的通知后,得到Dist文件夹,除了exe文件,还打包一堆第三方的包,一个窗口的文件竟然都有20M,文件夹内容和exe运行结果如下: