3
\$\begingroup\$

I decided to build a GUI that would seemlessly create user interfaces for validation functions, functions that check if a string satisfies given rules.

An example 'hello, world!' usage is:

from gui_validation import gui_validation

def is_valid_salutation(salutation):
    """
    Salutations to be valid must start with one of:
         ['hello', 'hi', 'howdy'] + ',' [COMMA]
    and must end with '!' [EXCLAMATION MARK]

    >>> is_valid_salutation('howdy, moon!')
    True
    >>> is_valid_salutation('random phrase')
    False
    """
    return any(salutation.startswith(i+',') for i in ['hello', 'hi', 'howdy']) \
           and salutation.endswith('!')

if __name__ == "__main__":
    gui_validation(is_valid_salutation)

As you can see the only argument required is the function itself.

gui_validation.py

"""
General purpose user input validation GUI.
"""
import tkinter
from tkinter.constants import X
from tkinter import messagebox

def gui_validation(validator, title=None, text=None, button_text="Validate"):
    """
    Provides a general purpose gui to validate user input.
    The user will be prompted to enter a string and will be
    given feedback by the `validator` function.

    This interface avoids verbosity and assumes the title to be
    the `validator` name and the text to be the `validator` doc if not +
    told explicitly.
    """
    if title is None:
        title = validator.__name__.replace('_',' ').capitalize()
    if text is None:
        text = validator.__doc__

    def validate():
        if validator(user_input.get()):
            messagebox.showinfo("Input valid.",
                                "Congratulations, you entered valid input.")
        else:
            messagebox.showerror("Input NOT valid.",
                                 "Please try again and enter a valid input.")

    root = tkinter.Tk()
    root.wm_title(title)

    title_label = tkinter.Label(root, text=title, font='25px')
    title_label.pack(fill=X, expand=1)

    text_label = tkinter.Label(root, text=text, font='20px')
    text_label.pack(fill=X, expand=1)

    user_input = tkinter.Entry(root)
    user_input.pack()

    button = tkinter.Button(root, text=button_text, command=validate)
    button.pack()

    root.mainloop()

I was wondering:

  • Would a class make the code clearer or just more verbose?
  • Am I asking too little, should I force the user to give more details?
  • Is it OK to use big fonts? The small fonts are much less legible to me.
  • Any other improvement?
\$\endgroup\$
  • \$\begingroup\$ When you say the "user", do you mean the person typing or the person developing the validation function(s)? \$\endgroup\$ – jonrsharpe Apr 28 '15 at 18:56
  • \$\begingroup\$ @jonrsharpe by user I mean developer in my question. \$\endgroup\$ – Caridorc Apr 28 '15 at 18:57
1
\$\begingroup\$

If the goal is "general purpose" I would be inclined to start from a much simpler premise, allowing users (i.e. developers) to build GUIs with the features that they need. Using object-oriented techniques you could subclass the tkinter widgets and extend them as needed. For example:

import tkinter as tk
from tkinter import messagebox


class ValidEntry(tk.Entry):

    def __init__(self, *args, validator=lambda text: True, **kwargs):
        super().__init__(*args, **kwargs)
        self.validator = validator

    def get(self):
        text = super().get()
        if not self.validator(text):
            raise ValueError('Invalid input')
        return text

This is now a reusable component; it can be dropped in wherever a regular tk.Entry is required (indeed the default validator allows it to be a direct replacement if validation isn't actually needed). The next step up is then:

class ValidatorFrame(tk.Frame):

    INSTRUCTION_FONT = '20px'

    def __init__(self, root, *args, button_text='Validate', instructions=None,
                 validator=lambda text: True, **kwargs):
        super().__init__(root, *args, **kwargs)
        if instructions is None:
            instructions = validator.__doc__
        self.instructions = tk.Label(
            root,
            font=INSTRUCTION_FONT,
            justify=tk.LEFT,
            text=instructions
        )
        self.instructions.pack(expand=1, fill=tk.X)
        self.user_input = ValidEntry(root, validator=validator)
        self.user_input.pack(expand=1, fill=tk.X)
        self.validate = tk.Button(root, command=self.validate, text=button_text)
        self.validate.pack()

    def validate(self):
        try:
            text = self.user_input.get()
        except ValueError:
            messagebox.showerror(
                "Input NOT valid.",
                "Please try again and enter a valid input."
            )
        else:
            messagebox.showinfo(
                "Input valid.",
                "Congratulations, you entered valid input."
            )

Again, this is a component that can be easily used elsewhere. One final step outward:

class TestGui(tk.Tk):

    TITLE_FONT = '25px'

    def __init__(self, *args, title=None, validator=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.title_label = tk.Label(self, font=self.TITLE_FONT, text=title)
        self.title_label.pack(expand=1, fill=tk.X)
        self.frame = ValidatorFrame(self, validator=validator)
        self.frame.pack(expand=1, fill=tk.X)
        if title is None:
            title = validator.__name__.replace('_', ' ').capitalize()
        self.wm_title(title)

By building up from basic components, you give yourself and your users a lot more flexibility. You can now launch this (using the validator you've already written) as simply as:

if __name__ == '__main__':
    app = TestGui(validator=is_valid_salutation)
    app.mainloop()

It's slightly more code, but a lot more flexible. One thing I haven't put much thought into is where the text would go once valid...


A few other things to note:

  • import tkinter as tk saves you repeating a few extra characters all over the place;
  • You tend to end up with a lot of keyword arguments to widgets, so I keep them in alphabetical order; and
  • Factoring out the styling information makes it easier to reuse, too.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.