一个典型的GUI应用程序能够抽象为:主界面(菜单栏、工具栏、状态栏、内容区域),二级界面(模态、非模态),信息提示(Tooltip),程序图标等组成。本篇根据做者使用PyQt5编写的一个工具,介绍如何使用PyQt5构建一个典型的GUI应用。
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 执行文件的图标,经过打包工具设置
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)顾名思义,将空间水平切成多段,而后经过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)
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_():
# -----
下面介绍编写工具过程当中使用到的组件的一些注意事项。
若是有不少列,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)
效果以下图所示:
默认的时间显示格式(如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)实现,很是的简单。
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 = "saylor.zhu@gmail.com",
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目录下便可。