一個典型的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 = "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目錄下即可。