QLineEdit是使用頻率最高的控制項之一,當我們想獲取用戶輸入時自然而然得會用到它。 通常我們會將QLineEdit的信號或其他控制項的信號綁定至槽函數,然後獲取並處理編輯器內的數據。你會覺得我們拿到的是第一手的“熱乎著”的數據,所以理所當然地將過濾和驗證邏輯都加入槽函數中,然而事實並非如此。那麼數 ...
QLineEdit是使用頻率最高的控制項之一,當我們想獲取用戶輸入時自然而然得會用到它。
通常我們會將QLineEdit的信號或其他控制項的信號綁定至槽函數,然後獲取並處理編輯器內的數據。你會覺得我們拿到的是第一手的“熱乎著”的數據,所以理所當然地將過濾和驗證邏輯都加入槽函數中,然而事實並非如此。那麼數據究竟通過了哪些流程最終才經由信號被我們獲取呢?
或者你希望QLineEdit能擁有自動補全或是輸入聯想的功能,這又如何實現呢?
如果你對上面的問題毫無頭緒,那麼本文就是為你量身打造的,請繼續往下閱讀吧!
本文索引
引論
這一節將帶你概覽QLineEdit對數據的處理,並以一個示例引出後續章節的內容。你可以先在此處找到一些粗淺的回答,後續則會有詳細的解釋。
如果要簡單的回答第一問,那麼在我們獲取到text內容前需要經過兩個步驟:
它們分別由inputMask
和QValidator
實現,前者負責過濾用戶的輸入,後者則用於過濾後的信息的驗證。
inputMask
和validator
的表現很相似,有時它們的功能還會有一些重合,那麼它們是否能取代彼此呢?答案是否定的,看起來像鴨子的鳥有時候其實不是和鴨子沒關係,後面我們仔細說明。
現在輪到回答第二個問題了。要實現補全和自動聯想,你只需要將一個設置好的QCompleter
對象傳遞給QLineEdit。是不是夠簡單?大部分時間也確實如此,然而“設置好的”這一形容詞的很抽象的概念,所以有的時候你可能要失望了,不過別擔心,後面我們也會詳細介紹它的使用。
兩問回答完畢,現在該來看看本文的示例了。這次我們將自己實現一個DateEdit
(我知道有現成的QDateEdit,不過這裡請允許我為了實踐所學而造一個粗糙的輪子),並根據用戶輸入的日期計算當天是周幾,效果如下:
實現CustomDateEdit
在本節中我們將逐步實現CustomDateEdit,並詳細介紹引論中提到的概念。
按照流程圖的順序,我們首先要講解的便是輸入數據的過濾——inputMask
的功能。
過濾用戶輸入——inputMask
在具體介紹一個能控制顯示效果的特性前,我習慣於先描述其大致功能和具體的顯示效果。
inputMask的功能:它是一串特定的規則,所有不符合規則的用戶輸入都會被丟棄,用戶不管是從信號還是text
槽都只能獲取符合mask要求的輸入數據,當然這個“用戶”包括我們後面要介紹的QValidator
及其派生類。
inputMask的顯示效果:你只能輸入合法的字元,輸入非法字元是輸入內容無法顯示,游標停留在原處;如果你設置了mask的填充字元,則這些字元會顯示在edit中,當輸入合法字元時將覆蓋它們,mask中的保留字元同樣顯示在edit中,但輸入時會被跳過不可覆蓋(類似占位符),引論中的效果圖就是很好的例子。
inputMask就是一串由特殊字元組成的規則,通過規則給定的格式來控制文本的輸入,具體的規則見下表:
特殊字元 | 對應規則 |
---|---|
A | 必須輸入的ascii字母,包括A-Z,a-z |
a | 和A 一樣,但是可選,也就是不輸入這個字元也可以,占位符將保留 |
N | 必須輸入的ascii字母和數字,包括A-Z,a-z,0-9 |
n | 和N 一樣,但是可選 |
X | 必須輸入的任意字元 |
x | 和X 一樣,但是可選 |
9 | 必填的ascii的數字字元,包括0-9 |
0 | 和9 一樣,但是可選 |
D | 必填的數字,包括1-9 |
d | 和D 一樣,但可選 |
# | 可選的數字或者加減號 |
H | 必填的16進位的數字,包括A-F, a-f, 0-9 |
h | 和H 一樣,但可選 |
B | 必填的二進位數字,包括0和1 |
b | 和B 一樣,但可選 |
> | 所有在這個特殊字元之後的字元轉換為大寫 |
< | 所有在這個特殊字元之後的字元轉換為小寫 |
! | 關閉前面的大小寫轉換 |
[ ] { } | 保留的特殊字元 |
\ | 將特殊字元轉義為普通字元 |
inputMask的格式為:([特殊字元]|[普通字元])*;占位符
,分號後跟的是占位符,用於填充特殊字元留下的空位,預設為空格。下麵看些例子:
000,000.00;_
:用於輸入一個最大6位,有兩位小數的值,用_
填充空位,edit會顯示出類似___,___.__
的效果>AAAA-AAAA!-AAAA-AAAA
:用於輸入一個由連字元分割的字母數字組成的uuid或license key,且前八個字母會被轉換為大寫,在edit中顯示為- - -
9999年09月09日
:用於輸入年月日的時間格式,可以輸入2019年03月14日
或2019年3月14日
,顯示效果在引論的效果圖中。- 空字元串:表示沒有任何輸入限制
你可以通過setInputMask
設置mask,或inputMask
獲取當前的mask。
通過上面的說明和例子你應該已經學會了inputMask的使用,現在可以看看它與validator的區別了:
- inputMask在用戶進行輸入時進行過濾,並且只存在符合規則和不符合兩種狀態,validator通常擁有第三種狀態
- inputMask只能過濾較為固定的格式,並且對於輸入的最大長度產生限制,validator則要靈活的多
最主要的區別是這兩點。上一節提到inputMask不能替代validator,現在我們揭曉原因:inputMask只能保證輸入數據的格式,但並不保證數據有意義,比如例子3中我們可以在月份上輸入20,但明顯日期中沒有20月,而這種錯誤是inputMask無法處理的,這就是為什麼我們說有時候一隻看起來像鴨子的鳥也許和鴨子沒有半點關係的原因。
因此想要獲得正確的數據,我們還需要驗證器來幫忙。
數據驗證——QValidator
現在該驗證我們的輸入了。因為有了inputMask的幫助,現在我們只需要驗證數據本身是否正確而不用操心它的格式了,真是謝天謝地。
等等,這麼說好像不太對,validator拿到的數據里居然還保留著mask的占位符?你沒看錯,這不是bug,能在edit里顯示出來的數據那麼一定能被獲得,mask本身的占位符是能通過過濾的,所以它會原封不動地傳給validator,只有用戶輸入合法的數據後這些占位符才會被覆蓋。所以在寫自己的驗證器的時候要小心了——我們需要先刪除所有的占位符,因為它們不是數據的一部分!
下麵我們來看看validator的功能和顯示效果。
功能:驗證數據是否合法,不合法會被丟棄,同時還要識別出數據是否輸入完成,這就是validator返回的第三種狀態。
顯示效果:和inputMask一樣。如果數據未輸入完則保留在edit中。
大致概覽後我們可以深入瞭解一下QValidator
了,所有的驗證器都是它的派生類。
QValidator
本身是一個純虛基類,派生類需要實現QValidator::State QValidator::validate(QString &input, int &pos) const
進行數據的驗證,還有一個可選的fixup
函數用於修複輸入,不過一般來說很少有自行修複輸入的需求,所以這裡使用預設的實現,也就是什麼都不做。
validate
驗證數據後返回數據是否合法,有QValidator::State
類型的值表示:
QValidator::Invalid
數據不合法QValidator::Intermediate
數據不完整需要進一步的輸入QValidator::Acceptable
數據合法
PyQt5中的介面稍微有些不同,處理第一個返回值的為QValidator::State
之外還需要把input
和pos
原封不動地作為第二和第三個值返回,否則edit無法正確顯示輸入的數據。
你可以通過validator
和setValidator
來獲取和設置驗證器。
因為額外引入了第三種狀態,所以實現一個validator遠比設置inputMask來的複雜,這裡我們實現一個自定義的日期驗證器用於配合CustomDateEdit
(我知道這個工作交給QRegExpValidator會很簡單),同時介紹如何實現一個驗證器。
下麵看看具體的代碼,首先我們不需要為validator額外增加內容,只需要實現幾個方法,因此不要要關註構造等行為:
class CustomDateValidator(QValidator):
"""驗證輸入的是否是合法的年月日
"""
def validate(self, input: str, pos: int):
date = input.replace(' ', '') # 去除占位符
y, m, d = self.splitDate(date)
if not (y and m and d):
return QValidator.Intermediate, input, pos
try:
arrow.get(date, self.dateFormat()) # 如果解析失敗代表日期輸入不合法
except Exception:
return QValidator.Invalid, input, pos
return QValidator.Acceptable, input, pos
def dateFormat(self):
"""返回arrow庫使用的日期解析格式,具體參見文檔,這裡與CustomDateEdit的inputMask保持一致
"""
return self.tr('YYYY年M月D日')
def splitDate(self, date: str):
"""分割日期成年,月,日,以便判斷數據是否輸入完整,
只要有某一部分為空就表明數據未輸入結束
"""
y, date = date.split(self.tr('年'))
m, date = date.split(self.tr('月'))
d = date.split(self.tr('日'))[0]
return y, m, d
可以看到驗證器的邏輯其實很簡單。整個驗證器加上幫助函數一共做了三件事:
- 首先去除占位符,如前文所述
- 接著將輸入信息按年月日分割,如果有某一部分為空則代表輸入不完整
- 對於完整的輸入則使用arrow解析成時間對象,失敗則表示輸入數據錯誤
其他的細節都已經在註釋中說明。
如此一來我們既驗證了數據的合法性又處理了所有可能的輸入情況。當然,通常我更建議你使用現有的QDoubleValidator
和QRegExpValidator
等現有的驗證器,或將它們組合使用,這樣更簡單也更不容易出錯。
自動補全——QCompleter
我們已經講解了輸入的過濾和驗證,最後該講講補全了。
可以說過濾和驗證是比較常用的功能的話,那補全就沒有那麼常見了。或者說,通常我們不需要關心它,比如QComboBox
自帶了QCompleter,它工作得也很好,所以我們往往忽略了它的存在。當然不只是下拉框,在QLineEdit
中我們也可以用它和它的派生類實現補全效果。
功能:QCompleter包含了一個叫completeModel的數據模型,裡面包含了用於根據輸入信息進行補全的所有數據,通常是個listModel,也可以是設置了補全所用數據位於哪一列的tableModel,當然你還可以用treeModel,不過這超過了我們的討論範圍。
顯示效果:completer從你輸入的第一個字元開始匹配,如果在completeModel中找到了以輸入內容開頭的信息則會在edit下把所有匹配項一次放入一個下拉框並顯示,你也可以設置為將第一個匹配項的數據替換放入edit。
還有一點我想額外補充一下,補全時彈出的下拉框其實是個view視圖對象,因此你可以選擇自己需要的視圖以顯示補全時想顯示的自定義效果。
你可以通過completer
和setCompleter
獲取和設置completer。
可以看到只要把我們用於補全輸入的數據放入合適的model中,再把model設置給completer,就能實現補全功能了。
下麵看個設置completer的例子:
# model是一個QStandardItemModel,後面我們也會使用這個model來設置completer
completer = QCompleter()
model.setParent(completer)
completer.setModel(model)
edit.setCompleter(completer)
另外completer得到的數據是經過驗證的,所以我們無需關心數據的格式和合法性。
現在我們已經把QLineEdit
的數據處理流程介紹了一遍,有了這些預備知識下麵該實現CustomDateEdit
了。
CustomDateEdit的實現
我們先來看代碼,細節問題基本在註釋中給出了說明:
class CustomDateEdit(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setInputMask(self.tr('9999年09月09日')) # 設置日期格式的inputMask
validator = CustomDateValidator()
self.setValidator(validator) # 設置validator
# 設置completer
self._completer = QCompleter()
self.setCompleter(self._completer)
self.completerModel = QStandardItemModel(parent=self._completer)
self._completer.setModel(self.completerModel)
# 預先填充一些待補全內容
self.addDateRecord("2019年03月14日")
self.addDateRecord("2019年03月15日")
def addDateRecord(self, text: str):
"""當有合法的輸入被確認時就將其添加至completerModel,以便再次輸入時補全
"""
if self.completerModel.findItems(text): # 避免重覆添加
return
item = QStandardItem(text)
self.completerModel.appendRow(item)
def weekDayInfo(self, weekDay: int):
"""返回weekDay對應的名稱,後面測試中會被使用
"""
week = {
0: self.tr('周一'),
1: self.tr('周二'),
2: self.tr('周三'),
3: self.tr('周四'),
4: self.tr('周五'),
5: self.tr('周六'),
6: self.tr('周日'),
}
return week[weekDay]
整個dateEdit的實現也很簡單,所有複雜的邏輯都已經交給了inputMask,驗證器和completer,而我們唯一要做的是為completer添加新輸入的合法的數據,這在類方法addDateRecord
中完成了。
測試CustomDateEdit
實現CustomDateEdit
之後,我們就要動手實現引論一節中的程式了。
前面已經說過,最終通過信號傳遞或者由槽函數獲取到的值一定是通過了過濾和驗證通過的值。所以想實現引論中的程式我們只需要正確處理CustomDateEdit
的信號即可。
下麵直接上測試代碼:
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent=parent)
center = QWidget()
self.dateEdit = CustomDateEdit()
self.info = QLabel(self.tr('所選日期是'))
self.dateEdit.textEdited.connect(lambda: self.info.setText(self.tr('所選日期是')))
# 輸入結束後按回車觸發該信號,同時只有輸入數據通過過濾和驗證後這個信號才會被髮送
self.dateEdit.returnPressed.connect(self.calcWeekDay)
layout = QVBoxLayout()
layout.addWidget(self.dateEdit)
layout.addWidget(self.info, alignment=Qt.AlignCenter)
center.setLayout(layout)
self.setCentralWidget(center)
def calcWeekDay(self):
# 計算所選日期是周幾
t = arrow.get(self.dateEdit.text(), self.dateEdit.validator().dateFormat())
weekDayInfo = self.dateEdit.weekDayInfo(t.weekday())
self.info.setText(self.tr('所選日期是') + weekDayInfo)
# 添加記錄
self.dateEdit.addDateRecord(self.dateEdit.text())
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
win.show()
app.exec_()
當用戶輸入一個完整的日期後,按下回車鍵,程式會自動計算結果並更新到下方的label上。很簡單的程式,主要就是為了測試我們的CustomDateEdit
:
程式的行為和預想的差不多,現在你已經初步掌握所學的知識了。
另外也許你會奇怪,為什麼要大量使用self.tr
這個函數,不用擔心,這隻是為了以後介紹國際化時做的準備,現在忽略它也沒問題。
如果你發現了任何錯誤疏漏,或者仍有疑問,歡迎提出,共同進步!