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


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

通常我們會將QLineEdit的信號或其他控件的信號綁定至槽函數,然后獲取並處理編輯器內的數據。你會覺得我們拿到的是第一手的“熱乎着”的數據,所以理所當然地將過濾和驗證邏輯都加入槽函數中,然而事實並非如此。那么數據究竟通過了哪些流程最終才經由信號被我們獲取呢?

或者你希望QLineEdit能擁有自動補全或是輸入聯想的功能,這又如何實現呢?

如果你對上面的問題毫無頭緒,那么本文就是為你量身打造的,請繼續往下閱讀吧!

本文索引

引論

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

如果要簡單的回答第一問,那么在我們獲取到text內容前需要經過兩個步驟:

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

inputMaskvalidator的表現很相似,有時它們的功能還會有一些重合,那么它們是否能取代彼此呢?答案是否定的,看起來像鴨子的鳥有時候其實不是和鴨子沒關系,后面我們仔細說明。

現在輪到回答第二個問題了。要實現補全和自動聯想,你只需要將一個設置好的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的格式為:([特殊字符]|[普通字符])*;占位符,分號后跟的是占位符,用於填充特殊字符留下的空位,默認為空格。下面看些例子:

  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這個函數,不用擔心,這只是為了以后介紹國際化時做的准備,現在忽略它也沒問題。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM