QLineEdit拾遺:數據的過濾、驗證和補全

QLineEdit是使用頻率最高的控件之一,當咱們想獲取用戶輸入時天然而然得會用到它。python

一般咱們會將QLineEdit的信號或其餘控件的信號綁定至槽函數,而後獲取並處理編輯器內的數據。你會以爲咱們拿到的是第一手的「熱乎着」的數據,因此理所固然地將過濾和驗證邏輯都加入槽函數中,然而事實並不是如此。那麼數據究竟經過了哪些流程最終才經由信號被咱們獲取呢?app

或者你但願QLineEdit能擁有自動補全或是輸入聯想的功能,這又如何實現呢?編輯器

若是你對上面的問題毫無頭緒,那麼本文就是爲你量身打造的,請繼續往下閱讀吧!函數

本文索引

引論

這一節將帶你概覽QLineEdit對數據的處理,並以一個示例引出後續章節的內容。你能夠先在此處找到一些粗淺的回答,後續則會有詳細的解釋。測試

若是要簡單的回答第一問,那麼在咱們獲取到text內容前須要通過兩個步驟:ui

它們分別由inputMaskQValidator實現,前者負責過濾用戶的輸入,後者則用於過濾後的信息的驗證。3d

inputMaskvalidator的表現很類似,有時它們的功能還會有一些重合,那麼它們是否能取代彼此呢?答案是否認的,看起來像鴨子的鳥有時候其實不是和鴨子不要緊,後面咱們仔細說明。code

如今輪到回答第二個問題了。要實現補全和自動聯想,你只須要將一個設置好的QCompleter對象傳遞給QLineEdit。是否是夠簡單?大部分時間也確實如此,然而「設置好的」這一形容詞的很抽象的概念,因此有的時候你可能要失望了,不過別擔憂,後面咱們也會詳細介紹它的使用。orm

兩問回答完畢,如今該來看看本文的示例了。此次咱們將本身實現一個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的格式爲:([特殊字符]|[普通字符])*;佔位符,分號後跟的是佔位符,用於填充特殊字符留下的空位,默認爲空格。下面看些例子:

  1. 000,000.00;_:用於輸入一個最大6位,有兩位小數的值,用_填充空位,edit會顯示出相似___,___.__的效果
  2. >AAAA-AAAA!-AAAA-AAAA:用於輸入一個由連字符分割的字母數字組成的uuid或license key,且前八個字母會被轉換爲大寫,在edit中顯示爲- - -
  3. 9999年09月09日:用於輸入年月日的時間格式,能夠輸入2019年03月14日2019年3月14日,顯示效果在引論的效果圖中。
  4. 空字符串:表示沒有任何輸入限制

你能夠經過setInputMask設置mask,或inputMask獲取當前的mask。

經過上面的說明和例子你應該已經學會了inputMask的使用,如今能夠看看它與validator的區別了:

  1. inputMask在用戶進行輸入時進行過濾,而且只存在符合規則和不符合兩種狀態,validator一般擁有第三種狀態
  2. 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以外還須要把inputpos原封不動地做爲第二和第三個值返回,不然edit沒法正確顯示輸入的數據。

你能夠經過validatorsetValidator來獲取和設置驗證器。

由於額外引入了第三種狀態,因此實現一個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

能夠看到驗證器的邏輯其實很簡單。整個驗證器加上幫助函數一共作了三件事:

  1. 首先去除佔位符,如前文所述
  2. 接着將輸入信息按年月日分割,若是有某一部分爲空則表明輸入不完整
  3. 對於完整的輸入則使用arrow解析成時間對象,失敗則表示輸入數據錯誤

其餘的細節都已經在註釋中說明。

如此一來咱們既驗證了數據的合法性又處理了全部可能的輸入狀況。固然,一般我更建議你使用現有的QDoubleValidatorQRegExpValidator等現有的驗證器,或將它們組合使用,這樣更簡單也更不容易出錯。

自動補全——QCompleter

咱們已經講解了輸入的過濾和驗證,最後該講講補全了。

能夠說過濾和驗證是比較經常使用的功能的話,那補全就沒有那麼常見了。或者說,一般咱們不須要關心它,好比QComboBox自帶了QCompleter,它工做得也很好,因此咱們每每忽略了它的存在。固然不僅是下拉框,在QLineEdit中咱們也能夠用它和它的派生類實現補全效果。

功能:QCompleter包含了一個叫completeModel的數據模型,裏面包含了用於根據輸入信息進行補全的全部數據,一般是個listModel,也能夠是設置了補全所用數據位於哪一列的tableModel,固然你還能夠用treeModel,不過這超過了咱們的討論範圍。

顯示效果:completer從你輸入的第一個字符開始匹配,若是在completeModel中找到了以輸入內容開頭的信息則會在edit下把全部匹配項一次放入一個下拉框並顯示,你也能夠設置爲將第一個匹配項的數據替換放入edit。

還有一點我想額外補充一下,補全時彈出的下拉框實際上是個view視圖對象,所以你能夠選擇本身須要的視圖以顯示補全時想顯示的自定義效果。

你能夠經過completersetCompleter獲取和設置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這個函數,不用擔憂,這只是爲了之後介紹國際化時作的準備,如今忽略它也沒問題。

若是你發現了任何錯誤疏漏,或者仍有疑問,歡迎提出,共同進步!

相關文章
相關標籤/搜索