一个典型的GUI应用程序可以抽象为:主界面(菜单栏、工具栏、状态栏、内容区域),二级界面(模态、非模态),信息提示(Tooltip),程序图标等组成。本篇根据作者使用PyQt5编写的一个工具,介绍如何使用PyQt5构建一个典型的GUI应用。
1. 主界面
QMainWindow类提供一个有菜单条、锚接窗口(例如工具条)和一个状态条的主应用程序窗口。主窗口通常用在提供一个大的中央窗口部件(例如文本编辑或者绘制画布)以及周围菜单、工具条和一个状态条。QMainWindow常常被继承,因为这使得封装中央部件、菜单和工具条以及窗口状态变得更容易。
菜单栏
创建菜单的代码如下:
self.addMenu = self.menuBar().addMenu("&添加") self.addMenu.addAction(self.addAvatarAct) self.addMenu.addAction(self.addAvatarSetAct) self.addMenu.addAction(self.addAvatarDecorationAct) self.modifyMenu = self.menuBar().addMenu("&修改") self.modifyMenu.addAction(self.modifyAvatarAct) self.modifyMenu.addAction(self.modifyAvatarSetAct) self.settingMenu = self.menuBar().addMenu("&设置") self.settingMenu.addAction(self.settingAct)
其中每个菜单项,关联一个QAction,定义了图标、菜单名、回调函数、快捷键等等,这里没有设置快捷键。
self.addAvatarAct = QAction(QIcon("res/ico/addAvatar.ico"), "&Add Avatar", self, triggered=self.addAvatar) self.addAvatarSetAct = QAction(QIcon("res/ico/addAvatarSet.ico"), "&Add AvatarSet", self, triggered=self.addAvatarSet) self.addAvatarDecorationAct = QAction(QIcon("res/ico/addAvatarDecoration.ico"), "&Add AvatarDecoration", self, triggered=self.addAvatarDecoration) self.modifyAvatarAct = QAction(QIcon("res/ico/modifyAvatar.ico"), "&Modify Avatar or Decoration", self, triggered=self.modifyAvatar) self.modifyAvatarSetAct = QAction(QIcon("res/ico/modifyAvatarSet.ico"), "&Modify AvatarSet", self, triggered=self.modifyAvatarSet) self.settingAct = QAction(QIcon("res/ico/settingPath.ico"), "&路径", self, triggered=self.settingPath) self.homeAct = QAction(QIcon("res/ico/home.ico"), "&首页", self, triggered=self.homePage)
说明:QAction类提供了一个可以同时出现在菜单和工具条上的抽象用户界面操作。
在图形用户界面应用程序中很多命令可以通过菜单选项、工具条按钮和键盘快捷键调用。因为同一个操作将会被执行,而与它的调用方法无关,并且因为菜单和工具条必须保持同步,所以提供一个操作这样的命令很有用。一个操作可以被添加到菜单和工具条中并且将会自动使它们同步。例如,如果用户按下“加粗”工具条按钮,“加粗”菜单项将会自动被选中。
QAction可以包含图标、菜单文本、快捷键、状态条文本、这是什么文本和工具提示。它们可以分别通过setIconSet()、setText()、setMenuText()、setToolTip()、setStatusTip()、setWhatsThis()和setAccel()来设置。
工具栏
创建工具栏的代码如下:
self.toolbar = self.addToolBar(‘Home‘) self.toolbar.addAction(self.homeAct) self.toolbar = self.addToolBar(‘AddAvatar‘) self.toolbar.addAction(self.addAvatarAct) self.toolbar = self.addToolBar(‘AddAvatarDecoration‘) self.toolbar.addAction(self.addAvatarDecorationAct) self.toolbar = self.addToolBar(‘AddAvatarSet‘) self.toolbar.addAction(self.addAvatarSetAct) self.toolbar = self.addToolBar(‘ModifyAvatar‘) self.toolbar.addAction(self.modifyAvatarAct) self.toolbar = self.addToolBar(‘ModifyAvatarSet‘) self.toolbar.addAction(self.modifyAvatarSetAct)
工具栏项也需要关联一个QAction,可以和菜单项共用一个QAction,即一个QAction可以被关联到多个地方。
状态栏
设置状态栏,只需要:
self.statusBar().showMessage("数据加载完成")
第一次调用self.statusBar()获取工具栏时,会初始化工具栏实例,后面再次调用不会在创建新的实例。
程序图标
程序图标分为2个:程序窗口图标;执行文件的图标。
l setWindowIcon(QIcon(“res/ico/icon.ico”))设置程序窗口的图标
l 执行文件的图标,通过打包工具设置
2. UI布局
PyQt的布局系统提供了一个规定子窗口部件布局的简单的和强有力的方式。当你一旦规定了合理的布局,你就会获得如下利益:
l 布置子窗口部件。
l 最高层窗口部件可感知的默认大小。
l 最高层窗口部件可感知的最小大小。
l 调整大小的处理。
l 当内容改变的时候自动更新:
n 字体大小、文本或者子窗口部件的其它内容。
n 隐藏或者显示子窗口部件。
n 移去一些子窗口部件。
PyQt支持的布局方式有很多,如下表所示:
布局相关类 |
作用 |
QBoxLayout |
Lines up child widgets horizontally or vertically |
QButtonGroup |
Container to organize groups of button widgets |
QFormLayout |
Manages forms of input widgets and their associated labels |
QGraphicsAnchor |
Represents an anchor between two items in a QGraphicsAnchorLayout |
QGraphicsAnchorLayout |
Layout where one can anchor widgets together in Graphics View |
QGridLayout |
Lays out widgets in a grid |
QGroupBox |
Group box frame with a title |
QHBoxLayout |
Lines up widgets horizontally |
QLayout |
The base class of geometry managers |
QLayoutItem |
Abstract item that a QLayout manipulates |
QSizePolicy |
Layout attribute describing horizontal and vertical resizing policy |
QSpacerItem |
Blank space in a layout |
QStackedLayout |
Stack of widgets where only one widget is visible at a time |
QStackedWidget |
Stack of widgets where only one widget is visible at a time |
QVBoxLayout |
Lines up widgets vertically |
QWidgetItem |
Layout item that represents a widget |
其中使用比较多的是以下布局方式(或者说是我使用比较多,不代表大家):
- 水平布局 QHBoxLayout
- 垂直布局 QVBoxLayout
- 网格布局 QGridLayout
水平布局
水平布局(QHBoxLayout)顾名思义,将空间水平切成多段,然后通过addWidget、addItem将widget填充指定的位置。如下代码即实现了上图中,适合角色选择的水平布局:
hbox = QHBoxLayout()
self.roleChkBoxGroup.setLayout(hbox)
for _, v in sorted(ParseKeywords.profession.items()):
checkBox = QRadioButton(v["cname"] + " " + str(v["value"]))
hbox.addWidget(checkBox)
删除一个控件,使用removeWidget,或者调用QWidget.hide()一样可以从布局中删除,直到QWidget.show()被调用。下面的垂直布局、网格布局,甚至其他布局都是注意的。
垂直布局
垂直布局(QVBoxLayout)顾名思义,将空间垂直切成多段,然后通过addWidget、addItem将widget填充指定的位置。如下代码即实现了上图中,细节信息的垂直布局(垂直布局中,还嵌套了水平布局):
vbox = QVBoxLayout()
groupBox.setLayout(vbox)
count = QWidget()
hbox = QHBoxLayout()
countLabel = QLabel("细节数目:")
hbox.addWidget(countLabel)
self.countSpineBox = QSpinBox()
self.countSpineBox.setRange(0, 10)
self.countSpineBox.valueChanged.connect(self.countSpineValueChanged)
hbox.addWidget(self.countSpineBox)
hbox.addStretch()
count.setLayout(hbox)
vbox.addWidget(count) #垂直布局,添加widget1
self.detailTable = QTableWidget()
self.detailTable.setColumnCount(9)
self.detailTable.setHorizontalHeaderLabels(
[‘有效期‘, ‘货币类型‘, ‘价格‘, ‘普通折扣价‘, ‘蓝钻价‘, ‘蓝钻折扣价‘, ‘超级蓝钻折扣价‘, ‘赠送礼包ID‘, ‘快捷购买‘])
vbox.addWidget(self.detailTable) #垂直布局,添加widget2
垂直布局中,还嵌套了水平布局。
说明:QHBoxLayout、QVBoxLayout都是继承自QBoxLayout,为了更好的控制布局,都继承了以下方法:
l QBoxLayout.addSpacing (size)
添加一个不能伸缩的空间(一个QSpacerItem),其宽度设置为size到布局末尾。框布局提供了默认的边距margin和spacing,这是额外添加的空间。
l QBoxLayout.addStretch(stretch)
添加一个可伸缩的空间(一个QSpacerItem),设0为最小值并且伸缩因子为stretch直到布局末尾
网络布局
网格布局(QGridLayout)顾名思义,将空间划分成多行多列的网络,然后通过addWidget、addItem将widget填充到指定的单元格(cell)。这个比较像网页中使用table布局的思路。下面的代码即创建上图中的网格布局:
grid = QGridLayout() grid.addWidget(setidLabel, 0, 0) grid.addWidget(self.setidLineEdit, 0, 1) grid.addWidget(QLabel("(第1位-2,第2~3位-表示适用角色,第4~5位-挂点位置,第6~8位-序号)"), 0, 2) grid.addWidget(subidLabel, 1, 0) grid.addWidget(self.subidLineEdit, 1, 1) grid.addWidget(QLabel("(套装包含的物品,多个物品适用逗号分隔;必须在套装之前添加)"), 1, 2) grid.addWidget(fashionLabel, 2, 0) grid.addWidget(self.fashionLineEdit, 2, 1) grid.addWidget(nameLabel, 3, 0) grid.addWidget(self.nameLineEdit, 3, 1) grid.addWidget(descLabel, 4, 0) grid.addWidget(self.descLineEdit, 4, 1) grid.addWidget(marketTagLabel, 5, 0) grid.addWidget(self.tagCombox, 5, 1) grid.addWidget(recommendLabel, 6, 0) grid.addWidget(self.recommendCombox, 6, 1) grid.addWidget(roleLabel, 8, 0) grid.addWidget(self.roleChkBoxGroup, 8, 1) grid.addWidget(beginLabel, 9, 0) grid.addWidget(self.beginTime, 9, 1) grid.addWidget(endLabel, 10, 0) grid.addWidget(self.endTime, 10, 1) gridWidget = QWidget() gridWidget.setLayout(grid)
上述往网格中添加的widget都是占一个单元格的情况,其实还支持占用几个单元格。如下代码,往网格中的第二行、第一列添加一个widget,占用1行、2列:
grid.addWidget(self.createDetail(), 1, 0, 1, 2)
网格布局默认是均分每列,为了更好的控制布局,QGridLayout为每列提供了最小宽度(setColumnMinimumWidth())、伸缩因子(setColumnStretch()),为每行提供了最小高度(setRowMinimumHeight())、伸缩因子(setRowStretch())。最小宽/高度很好理解,伸缩因子如下面代码,设置了第二列和三列的比例是1:2。
layout.setColumnStretch(1, 10)
layout.setColumnStretch(2, 20)
3. 二级弹窗
QDialog类是对话框窗口的基类。对话框窗口是主要用于短期任务以及和用户进行简要通讯的顶级窗口。QDialog可以是模态对话框也可以是非模态对话框。QDialog支持扩展性并且可以提供返回值。它们可以有默认按钮。
内置对话框
内置常用的对话框有:QColorDialog、QErrorMessage、QFileDialog、QFontDialog、QInputDialog、QMessageBox、QProgressDialog、QTabDialog、QWizard。
内置的对话框提供了一些常用的功能,使用起来也必将遍历。编写该工具使用到了,选择文件、目录的对话框QFileDialog。
自定义对话框
如果内置的对话框不能满足需求,可以自定义对话框(继承自QDialog)。如下定义了一个设置路径的对话框:
class SettingDialog(QDialog): def __init__(self, parent=None): super(SettingDialog, self).__init__(parent) self.path = Global.path self.initUI() self.setWindowIcon(QIcon("res/ico/settingPath.ico")) self.setWindowTitle("设置") self.resize(240, 100) def initUI(self): grid = QGridLayout() grid.addWidget(QLabel("路径:"), 0, 0) self.pathLineEdit = QLineEdit() self.pathLineEdit.setFixedWidth(200) self.pathLineEdit.setText(Global.path) grid.addWidget(self.pathLineEdit, 0, 1) button = QPushButton("更改") button.clicked.connect(self.changePath) grid.addWidget(button, 0, 2) grid.addWidget(QLabel("<font color=‘#ff0000‘>包含Keywords.xml、Avatar,AvatarSet,Market.xls的路径</font>"), 1, 0, 1, 3) buttonBox = QDialogButtonBox() buttonBox.setOrientation(Qt.Horizontal) # 设置为水平方向 buttonBox.setStandardButtons(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) buttonBox.accepted.connect(self.accept) # 确定 buttonBox.rejected.connect(self.reject) # 取消 grid.addWidget(buttonBox, 2, 1) self.setLayout(grid) def changePath(self): open = QFileDialog() self.path = open.getExistingDirectory() self.pathLineEdit.setText(self.path) print(self.path) 使用对话框,只需要: dialog = SettingDialog() if dialog.exec_(): # -----
4. 常用组件
下面介绍编写工具过程中使用到的组件的一些注意事项。
QTableWidget
列自适应
如果有很多列,QTableWidget出出现水平滚动条,但是有不希望有滚动条可以通过设置列自适应方式:
tw.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
保证所以列都能显示,不会出现水平滚动条,这样有的单元格显示会被截断显示,如图中的"青年套装下装"-->"青年套装...",这时可以设置单元的tooltip提供完整显示的途径。
禁止编辑
编写工具时,有要求QTableWidget展示出来的数据不能编辑,是通过以下方式实现:
tw.setEditTriggers(QAbstractItemView.NoEditTriggers)
QAbstractItemView还定义了其它的模式,如下表所示:
Constant |
Value |
Description |
QAbstractItemView.NoEditTriggers |
0 |
No editing possible. |
QAbstractItemView.CurrentChanged |
1 |
Editing start whenever current item changes. |
QAbstractItemView.DoubleClicked |
2 |
Editing starts when an item is double clicked. |
QAbstractItemView.SelectedClicked |
4 |
Editing starts when clicking on an already selected item. |
QAbstractItemView.EditKeyPressed |
8 |
Editing starts when the platform edit key has been pressed over an item. |
QAbstractItemView.AnyKeyPressed |
16 |
Editing starts when any key is pressed over an item. |
QAbstractItemView.AllEditTriggers |
31 |
Editing starts for all above actions. |
按行选择
设置QTableWidget按行选择:
tw.setSelectionBehavior(QAbstractItemView::SelectRows); //整行选中的方式
QAbstractItemView还定义了其它的模式,如下表所示:
Constant |
Value |
Description |
QAbstractItemView.SelectItems |
0 |
Selecting single items. |
QAbstractItemView.SelectRows |
1 |
Selecting only rows. |
QAbstractItemView.SelectColumns |
2 |
Selecting only columns. |
表头排序
如果希望单击QTableWidget表头进行数据排序,可以简单通过以下接口实现:
tw.setSortingEnabled(True)
但是,排序需要注意的2个问题:
l 点了下qtablewidget 的标题,它排序正常,修改数据,在查询,数据显示有问题
重新获取数据之前先关闭可排序性,获取到数据之后再开启排序性
l 排序规则问题,默认使用字母排序
使用以下方式设置单元格,会使用字母排序
item = QTableWidgetItem()
item.setData(Qt.DisplayRole, "xxx")
或者
item = QTableWidgetItem()
item.setText("xxx")
如果需要按照数值排序需要使用以下方式设置单元格
item = QTableWidgetItem()
item.setData(Qt.DisplayRole, int(1212))
自定义单元格控件
可以对QTableWidget自定义(添加)widget,如下为QTableWidget设置单元格为一个下拉选择的QCombox
combox = QComboBox()
for _, v in ParseKeyword.currencyType.items():
combox.addItem(v["cname"], v["value"])
combox.setCurrentText("点券")
tw.setCellWidget(row, 1, combox)
效果如下图所示:
QDateTimeEdit
显示格式
默认的时间显示格式(如2015/1/16 17:42),可能不满足需求,可以通过setDisplayFormat()设置显示格式来定制。格式选项如下所示:
这些是可能用到的日期表达式:
l d - 没有前置0的数字的天(1-31)
l dd - 前置0的数字的天(01-31)
l ddd - 缩写的日名称(Mon-Sun)。使用QDate.shortDayName()。
l dddd - 长的日名称(Monday-Sunday)。使用QDate.longDayName()。
l M - 没有前置0的数字的月(1-12)
l MM - 前置0的数字的月(01-12)
l MMM - 缩写的月名称(Jan-Dec)。使用QDate.shortMonthName()。
l MMMM - 长的月名称(January-December)。使用QDate.longMonthName()。
l yy - 两位数字的年(00-99)
l yyyy - 四位数字的年(0000-9999)
这些是可能用到的时间表达式:
l h - 没有前置0的数字的小时(0-23或者如果显示AM/PM时,1-12)
l hh - 前置0的数字的小时(00-23或者如果显示AM/PM时,01-12)
l m - 没有前置0的数字的分钟(0-59)
l mm - 前置0的数字的分钟(00-59)
l s - 没有前置0的数字的秒(0-59)
l ss - 前置0的数字的秒(00-59)
l z - 没有前置0的数字的毫秒(0-999)
l zzz - 前置0的数字的毫秒(000-999)
l AP - 切换为AM/PM显示。AP将被“AM”或“PM”替换。
l ap - 切换为am/pm显示。ap将被“am”或“pm”替换。
如工具中使用的格式为:
setDisplayFormat("yyyy-MM-dd hh:mm:ss")
显示效果如下图所示:
弹出日期选择窗口
希望点击QDateTimeEdit可以弹出日期选择窗口,可以简单的通过setCalendarPopup(True)实现,非常的简单。
5. 打包
python常用的打包工具有py2exe、pyinstaller、cx_freeze,而且现在都开始支持python3,py2exe可以打包成单exe文件,一般简单的东西都是用它来打包供其他人使用。但是使用py2exe打包PyQt5时,碰到了不少错误,后面干脆使用cx_freeze打包一次成功(不足之处,就是不能打包成单个exe)。下面简单介绍编写setup.py几个关键的点,详细的参考官方文档(http://cx-freeze.readthedocs.org/en/latest/index.html)。
l 默认只会打包代码文件,如果程序有非代码文件,如配置、资源文件需要打包,需要显示指定。如"include_files": ["setting.ini", "res"],打包时会将setting.ini文件、res资源目录拷贝到exe目录下。
l cx_freeze会自动检测依赖文件,但是有时候会抽风,可以通过"packages": ["os", "xlrd3", "xlwt3", "lxml"]显示包含。同时对不要的包,可以"excludes": ["tkinter"]指定不要编译到最终的软件包中。
l 指定文件名需要带exe后缀,cx_freeze是不会自动添加exe后缀的。
l 如果需要一次编译多个exe,可以在executables数组中列出多个,例如:
executables = [
Executable("main.py", base=base, targetName="Joker3DAvatarMgr.exe", compress=True, icon="res/ico/icon.ico"),
Executable("test.py", base=base, targetName="test.exe", compress=True, icon="res/ico/test.ico")
]
完整的setup.py文件如下所示:
import sys from cx_Freeze import setup, Executable # GUI applications require a different base on Windows (the default is for a # console application). base = None if sys.platform == "win32": base = "Win32GUI" # Dependencies are automatically detected, but it might need fine tuning. build_exe_options = { "packages": ["os", "xlrd3", "xlwt3", "lxml"], "excludes": ["tkinter"], "include_files": ["setting.ini", "res"] } # executables = [ Executable("main.py", base=base, targetName="Joker3DAvatarMgr.exe", compress=True, icon="res/ico/icon.ico") ] setup( name = "setup", version = "0.1", description = "Joker3D prop manager tool!", author = "tylerzhu", author_email = "[email protected]", options = {"build_exe": build_exe_options}, executables = executables, )
编写好setup.py之后,可以通过python setup.py build打包。
网上有不少人反馈打包之后,放到没有按照PyQt的PC上执行,会报以下错误:“This application failed to start because it could not find or load the Qt platform plugin windows”
这个问题,我以前也碰到过,但是这次我用的Python3.4 + cx_freeze 4.3.4 + PyQt5-5.4-gpl-Py3.4-Qt5.4.0-x32.exe并没有出现这个问题。如果出现了这个问题也不要紧,通过以下方法可以解决:将PyQt5安装目录(Lib\site-packages\PyQt5)下的libGLESv2.dll拷到打包的exe目录下即可。