Xác thực tương tác nội dung tiện ích con Entry trong tkinter


85

Kỹ thuật được đề xuất để xác thực tương tác nội dung trong Entrytiện ích tkinter là gì?

Tôi đã đọc các bài đăng về cách sử dụng validate=Truevalidatecommand=command, và có vẻ như các tính năng này bị hạn chế bởi thực tế là chúng sẽ bị xóa nếu validatecommandlệnh cập nhật Entrygiá trị của tiện ích.

Với hành vi này, chúng ta nên ràng buộc trên KeyPress, CutPastecác sự kiện và theo dõi / cập nhật của chúng tôi Entrygiá trị phụ tùng thông qua những sự kiện này? (Và các sự kiện liên quan khác mà tôi có thể đã bỏ lỡ?)

Hay chúng ta nên quên hoàn toàn xác thực tương tác và chỉ xác thực trên FocusOutcác sự kiện?

Câu trả lời:


217

Câu trả lời đúng là, sử dụng validatecommandthuộc tính của tiện ích con. Thật không may, tính năng này ít được ghi lại trong thế giới Tkinter, mặc dù nó được ghi lại khá đầy đủ trong thế giới Tk. Mặc dù nó không được ghi chép đầy đủ, nhưng nó có mọi thứ bạn cần để xác thực mà không cần dùng đến các ràng buộc hoặc biến truy tìm hoặc sửa đổi tiện ích con từ bên trong thủ tục xác thực.

Bí quyết là bạn có thể yêu cầu Tkinter chuyển các giá trị đặc biệt vào lệnh xác thực của mình. Các giá trị này cung cấp cho bạn tất cả thông tin bạn cần biết để quyết định xem dữ liệu có hợp lệ hay không: giá trị trước khi chỉnh sửa, giá trị sau khi chỉnh sửa nếu chỉnh sửa hợp lệ và một số bit thông tin khác. Tuy nhiên, để sử dụng chúng, bạn cần thực hiện một chút voodoo để chuyển thông tin này tới lệnh xác thực của bạn.

Lưu ý: điều quan trọng là lệnh xác thực trả về Truehoặc False. Bất kỳ điều gì khác sẽ khiến cho tiện ích con bị tắt xác thực.

Đây là một ví dụ chỉ cho phép viết thường (và in tất cả các giá trị sôi nổi đó):

import tkinter as tk  # python 3.x
# import Tkinter as tk # python 2.x

class Example(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        # valid percent substitutions (from the Tk entry man page)
        # note: you only have to register the ones you need; this
        # example registers them all for illustrative purposes
        #
        # %d = Type of action (1=insert, 0=delete, -1 for others)
        # %i = index of char string to be inserted/deleted, or -1
        # %P = value of the entry if the edit is allowed
        # %s = value of entry prior to editing
        # %S = the text string being inserted or deleted, if any
        # %v = the type of validation that is currently set
        # %V = the type of validation that triggered the callback
        #      (key, focusin, focusout, forced)
        # %W = the tk name of the widget

        vcmd = (self.register(self.onValidate),
                '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
        self.entry = tk.Entry(self, validate="key", validatecommand=vcmd)
        self.text = tk.Text(self, height=10, width=40)
        self.entry.pack(side="top", fill="x")
        self.text.pack(side="bottom", fill="both", expand=True)

    def onValidate(self, d, i, P, s, S, v, V, W):
        self.text.delete("1.0", "end")
        self.text.insert("end","OnValidate:\n")
        self.text.insert("end","d='%s'\n" % d)
        self.text.insert("end","i='%s'\n" % i)
        self.text.insert("end","P='%s'\n" % P)
        self.text.insert("end","s='%s'\n" % s)
        self.text.insert("end","S='%s'\n" % S)
        self.text.insert("end","v='%s'\n" % v)
        self.text.insert("end","V='%s'\n" % V)
        self.text.insert("end","W='%s'\n" % W)

        # Disallow anything but lowercase letters
        if S == S.lower():
            return True
        else:
            self.bell()
            return False

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()

Để biết thêm thông tin về những gì sẽ xảy ra khi bạn gọi registerphương thức, hãy xem Xác thực đầu vào tkinter


15
Đây là cách đúng đắn để làm điều đó. Nó giải quyết các vấn đề tôi tìm thấy khi tôi cố gắng làm cho câu trả lời của jmeyer10 hoạt động. Một ví dụ này cung cấp tài liệu cao cấp để xác thực so với những gì tôi có thể tìm thấy ở những nơi khác. Tôi ước tôi có thể cho 5 phiếu bầu này.
Steven Rumbalski

3
WOW! Tôi đồng ý với Steven - đây là kiểu trả lời xứng đáng hơn một phiếu bầu. Bạn nên viết một cuốn sách trên Tkinter (và bạn đã đăng đủ giải pháp để tạo nên một bộ sách nhiều tập). Cảm ơn bạn!!!
Malcolm

2
Cảm ơn vì ví dụ. Cần lưu ý rằng validatecommand PHẢI trả về boolean (chỉ True và False). Nếu không, xác thực sẽ bị xóa.
Dave Bacher

3
Tôi nghĩ rằng trang này nên được đưa lên hàng đầu.
Chân phải

4
"được ghi chép đầy đủ trong thế giới Tkinter". LOL - giống như hầu hết phần còn lại của thế giới Tkiinter.
martineau

21

Sau khi nghiên cứu và thử nghiệm với mã của Bryan, tôi đã tạo ra một phiên bản xác thực đầu vào tối thiểu. Đoạn mã sau sẽ đưa ra một hộp Entry và chỉ chấp nhận các chữ số.

from tkinter import *

root = Tk()

def testVal(inStr,acttyp):
    if acttyp == '1': #insert
        if not inStr.isdigit():
            return False
    return True

entry = Entry(root, validate="key")
entry['validatecommand'] = (entry.register(testVal),'%P','%d')
entry.pack()

root.mainloop()

Có lẽ tôi nên nói thêm rằng tôi vẫn đang học Python và tôi sẽ sẵn lòng chấp nhận bất kỳ và tất cả các nhận xét / đề xuất.


1
Nói chung mọi người sử dụng entry.configure(validatecommand=...)và viết test_valthay vì testVal, nhưng đây là một ví dụ tốt, đơn giản.
wizzwizz 4

10

Sử dụng a Tkinter.StringVarđể theo dõi giá trị của tiện ích Entry. Bạn có thể xác nhận giá trị của StringVarbằng cách đặt một tracetrên đó.

Đây là một chương trình làm việc ngắn chỉ chấp nhận các float hợp lệ trong tiện ích Entry.

from Tkinter import *
root = Tk()
sv = StringVar()

def validate_float(var):
    new_value = var.get()
    try:
        new_value == '' or float(new_value)
        validate.old_value = new_value
    except:
        var.set(validate.old_value)    
validate.old_value = ''

# trace wants a callback with nearly useless parameters, fixing with lambda.
sv.trace('w', lambda nm, idx, mode, var=sv: validate_float(var))
ent = Entry(root, textvariable=sv)
ent.pack()

root.mainloop()

1
Cảm ơn cho bài viết của bạn. Tôi rất thích khi thấy phương thức Tkinter StringVar .trace () được sử dụng.
Malcolm

4

Trong khi nghiên cứu câu trả lời của Bryan Oakley , điều gì đó nói với tôi rằng một giải pháp tổng quát hơn có thể được phát triển. Ví dụ sau đây giới thiệu một kiểu liệt kê, một từ điển kiểu và một chức năng thiết lập cho mục đích xác thực. Xem dòng 48 để biết cách sử dụng ví dụ và minh họa về tính đơn giản của nó.

#! /usr/bin/env python3
# /programming/4140437
import enum
import inspect
import tkinter
from tkinter.constants import *


Mode = enum.Enum('Mode', 'none key focus focusin focusout all')
CAST = dict(d=int, i=int, P=str, s=str, S=str,
            v=Mode.__getitem__, V=Mode.__getitem__, W=str)


def on_validate(widget, mode, validator):
    # http://www.tcl.tk/man/tcl/TkCmd/ttk_entry.htm#M39
    if mode not in Mode:
        raise ValueError('mode not recognized')
    parameters = inspect.signature(validator).parameters
    if not set(parameters).issubset(CAST):
        raise ValueError('validator arguments not recognized')
    casts = tuple(map(CAST.__getitem__, parameters))
    widget.configure(validate=mode.name, validatecommand=[widget.register(
        lambda *args: bool(validator(*(cast(arg) for cast, arg in zip(
            casts, args)))))]+['%' + parameter for parameter in parameters])


class Example(tkinter.Frame):

    @classmethod
    def main(cls):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.title('Validation Example')
        cls(root).grid(sticky=NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        root.mainloop()

    def __init__(self, master, **kw):
        super().__init__(master, **kw)
        self.entry = tkinter.Entry(self)
        self.text = tkinter.Text(self, height=15, width=50,
                                 wrap=WORD, state=DISABLED)
        self.entry.grid(row=0, column=0, sticky=NSEW)
        self.text.grid(row=1, column=0, sticky=NSEW)
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(0, weight=1)
        on_validate(self.entry, Mode.key, self.validator)

    def validator(self, d, i, P, s, S, v, V, W):
        self.text['state'] = NORMAL
        self.text.delete(1.0, END)
        self.text.insert(END, 'd = {!r}\ni = {!r}\nP = {!r}\ns = {!r}\n'
                              'S = {!r}\nv = {!r}\nV = {!r}\nW = {!r}'
                         .format(d, i, P, s, S, v, V, W))
        self.text['state'] = DISABLED
        return not S.isupper()


if __name__ == '__main__':
    Example.main()

4

Câu trả lời của Bryan là chính xác, tuy nhiên không ai đề cập đến thuộc tính 'invalidcommand' của tiện ích tkinter.

Giải thích hay ở đây: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Sao chép / dán văn bản trong trường hợp liên kết bị hỏng

Tiện ích con Entry cũng hỗ trợ tùy chọn lệnh không hợp lệ chỉ định một hàm gọi lại được gọi bất cứ khi nào lệnh hợp lệ trả về giá trị Sai. Lệnh này có thể sửa đổi văn bản trong tiện ích con bằng cách sử dụng phương thức .set () trên văn bản có thể liên quan của tiện ích con. Thiết lập tùy chọn này hoạt động giống như thiết lập câu lệnh xác thực. Bạn phải sử dụng phương thức .register () để bọc hàm Python của mình; phương thức này trả về tên của hàm được bọc dưới dạng một chuỗi. Sau đó, bạn sẽ chuyển dưới dạng giá trị của tùy chọn lệnh không hợp lệ hoặc là chuỗi đó hoặc dưới dạng phần tử đầu tiên của bộ mã có chứa mã thay thế.

Lưu ý: Chỉ có một điều mà tôi không thể tìm ra cách thực hiện: Nếu bạn thêm xác thực vào mục nhập và người dùng chọn một phần của văn bản và nhập giá trị mới, thì không có cách nào để nắm bắt giá trị ban đầu và đặt lại mục nhập. Đây là một ví dụ

  1. Mục nhập được thiết kế để chỉ chấp nhận các số nguyên bằng cách triển khai 'validatecommand'
  2. Người dùng nhập 1234567
  3. Người dùng chọn '345' và nhấn 'j'. Điều này được đăng ký dưới dạng hai hành động: xóa '345' và chèn 'j'. Tkinter bỏ qua việc xóa và chỉ hoạt động trên việc chèn 'j'. 'validatecommand' trả về False và các giá trị được truyền cho hàm 'validatecommand' như sau:% d = 1,% i = 2,% P = 12j67,% s = 1267,% S = j
  4. Nếu mã không triển khai hàm 'lệnh không hợp lệ', hàm 'lệnh hợp lệ' sẽ từ chối lệnh 'j' và kết quả sẽ là 1267. Nếu mã thực hiện hàm 'lệnh không hợp lệ', không có cách nào để khôi phục 1234567 ban đầu .

3

Đây là một cách đơn giản để xác thực giá trị nhập, cho phép người dùng chỉ nhập các chữ số:

import tkinter  # imports Tkinter module


root = tkinter.Tk()  # creates a root window to place an entry with validation there


def only_numeric_input(P):
    # checks if entry's value is an integer or empty and returns an appropriate boolean
    if P.isdigit() or P == "":  # if a digit was entered or nothing was entered
        return True
    return False


my_entry = tkinter.Entry(root)  # creates an entry
my_entry.grid(row=0, column=0)  # shows it in the root window using grid geometry manager
callback = root.register(only_numeric_input)  # registers a Tcl to Python callback
my_entry.configure(validate="key", validatecommand=(callback, "%P"))  # enables validation
root.mainloop()  # enters to Tkinter main event loop

Tái bút: Ví dụ này có thể rất hữu ích để tạo một ứng dụng như calc.


2
import tkinter
tk=tkinter.Tk()
def only_numeric_input(e):
    #this is allowing all numeric input
    if e.isdigit():
        return True
    #this will allow backspace to work
    elif e=="":
        return True
    else:
        return False
#this will make the entry widget on root window
e1=tkinter.Entry(tk)
#arranging entry widget on screen
e1.grid(row=0,column=0)
c=tk.register(only_numeric_input)
e1.configure(validate="key",validatecommand=(c,'%P'))
tk.mainloop()
#very usefull for making app like calci

2
Xin chào, chào mừng bạn đến với Stack Overflow. Các câu trả lời "chỉ có mã" sẽ không được như ý muốn, đặc biệt là khi trả lời một câu hỏi đã có nhiều câu trả lời. Hãy đảm bảo thêm một số thông tin chi tiết bổ sung về lý do tại sao phản hồi bạn đang cung cấp bằng cách nào đó thực chất và không chỉ lặp lại những gì đã được người đăng ban đầu kiểm tra.
chb

1
@Demian Wolf Tôi thích phiên bản cải tiến của câu trả lời gốc của bạn, nhưng tôi phải lùi lại. Vui lòng xem xét đăng nó như một câu trả lời của riêng bạn (bạn có thể tìm thấy nó trong lịch sử sửa đổi ).
Marc.2377

1

Đáp lại vấn đề của orionrobert trong việc xử lý xác thực đơn giản khi thay thế văn bản thông qua lựa chọn, thay vì xóa hoặc chèn riêng biệt:

Việc thay thế văn bản đã chọn được xử lý dưới dạng xóa, sau đó là chèn. Điều này có thể dẫn đến các vấn đề, ví dụ, khi việc xóa sẽ di chuyển con trỏ sang trái, trong khi thay thế sẽ di chuyển con trỏ sang phải. May mắn thay, hai quá trình này được thực thi ngay lập tức sau một quá trình khác. Do đó, chúng ta có thể phân biệt giữa việc tự xóa và xóa trực tiếp theo sau là chèn do thay thế vì sau đó không thay đổi cờ nhàn rỗi giữa xóa và chèn.

Điều này được khai thác bằng cách sử dụng thay thếFlag và a Widget.after_idle(). after_idle()thực thi lambda-function ở cuối hàng đợi sự kiện:

class ValidatedEntry(Entry):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.tclValidate = (self.register(self.validate), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
        # attach the registered validation function to this spinbox
        self.config(validate = "all", validatecommand = self.tclValidate)

    def validate(self, type, index, result, prior, indelText, currentValidationMode, reason, widgetName):

        if typeOfAction == "0":
            # set a flag that can be checked by the insertion validation for being part of the substitution
            self.substitutionFlag = True
            # store desired data
            self.priorBeforeDeletion = prior
            self.indexBeforeDeletion = index
            # reset the flag after idle
            self.after_idle(lambda: setattr(self, "substitutionFlag", False))

            # normal deletion validation
            pass

        elif typeOfAction == "1":

            # if this is a substitution, everything is shifted left by a deletion, so undo this by using the previous prior
            if self.substitutionFlag:
                # restore desired data to what it was during validation of the deletion
                prior = self.priorBeforeDeletion
                index = self.indexBeforeDeletion

                # optional (often not required) additional behavior upon substitution
                pass

            else:
                # normal insertion validation
                pass

        return True

Tất nhiên, sau khi thay thế, trong khi xác thực phần xóa, người ta vẫn sẽ không biết liệu phần chèn có theo sau hay không. May mắn tuy nhiên, với: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), chúng ta có thể đạt được hầu hết các hành vi mong muốn truy (kể từ sự kết hợp của substitutionFlag mới của chúng tôi với một chèn là một sự kiện độc đáo và cuối cùng mới.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.