Ввод данных общего назначения GUI с проверкой, но неясным о лучшем дизайне объекта

Когда я собираю код прототипа, я не хочу тратить дополнительное время на настройку параметров или преследовать проблемы от неправильного ввода параметров. Таким образом, это общая цель, довольно простая в использовании, помощница ввода данных GUI, которая, надеюсь, позаботится о большинстве случаев ввода данных, которые срывают работу только с тем, чтобы работать с проклятым целевым кодом.

У меня действительно были проблемы с принятием решения о том, должна ли каждая строка ввода быть самостоятельным объектом. Показанный здесь код использует такой подход, но в качестве недостатка ширина поля фиксируется так, что они хорошо выстраиваются в линию, и есть обратный вызов обновления. В более ранней версии виджет верхнего уровня построил все, от индивидуального Label, Entry и Button, которые автоматически заботились о выстроенном макете и не нуждались в обновлении обратный вызов, но был немного неравномерен вокруг хранения соответствующих элементов и необходимости закрытия для обратных вызовов трассировки для проверки правильных данных.

Я изменил, имел ли я несколько списков ключей и строк ввода, или dict записей, введенных ключами несколько раз, и не кажется действительно аккуратный, хотя реализация списков у меня сейчас не так уж плоха.

Во всяком случае, он работает. Я пропустил лучший способ его разработки, чтобы мне было лучше? Он имеет __name__ == '__main__', поэтому запустите его и выполните игру.

""" class to use as a general purpose GUI input widget
and a few convenience functions for validating data inputs
"""

import tkinter as tki
import tkinter.messagebox as tkm

class MyLabelEntry(tki.Frame):
    """ a combination of label, entry and help button, for validated gui entry of data"""
    def __init__(self, parent, text='label', data='', conv=None, update=None):
        """ text    optional    used to label the entry

            data    optional    used to initialise the Entry box
                                uses empty string if none supplied

            conv    optional    conversion function, which also validates the entry
                                note that int and float can be used
                                return string unchanged if omitted or None
                                return object if entry is valid
                                its __doc__ string is available as help
                                if the docstring starts with [x], x is put on the help button
                                (have to work on tooltips sometime!)
                                raise ValueError if the entry is invalid
                                The err from ValueError is saved for the help button
                                so more info can be given about failure to validate

        """
        tki.Frame.__init__(self, master=parent)
        self.update = update

        # do the properties
        self.err = ''
        self.value = None

        # do the label
        self.label = tki.Label(self, text=text, width=15)
        self.label.grid(row=0, column=0)

        # do the conversion function
        self.conv = conv
        cdoc = conv.__doc__  # easier to type
        if conv:
            # we have a conversion function specified
            # is it one of the builtins?
            if conv == int:
                help_face = 'i'
                self.conv_help = 'builtin int() function'
            elif conv == float:
                help_face = 'f'
                self.conv_help = 'builtin float() function'
            else:
                # neither of those, so does it have a docstring?
                if cdoc:
                    # yes, does it start with a help_face?
                    face_end = cdoc.find(']')
                    if (cdoc[0] == '[') and (face_end != -1) and (face_end < 6):
                        help_face = cdoc[1:face_end]
                    else:
                        help_face = '?'
                    # is the help prompt truncated in the docstring?
                    help_end = cdoc.find('[end_help]')
                    if help_end != -1:
                        self.conv_help = cdoc[:help_end]
                    else:
                        self.conv_help = cdoc                    
                else:
                    self.conv_help = 'no documentation\nfor this conversion'
                    help_face = '?'
        else:
            self.conv = str
            help_face = '='
            self.conv_help = 'unmodified string'

        # do the entry
        self.var = tki.StringVar()
        self.entry = tki.Entry(self, textvariable=self.var, width = 15)
        self.entry.grid(row=0, column=1)
        self.var.trace('w', self._changed)

        # do the help button
        self.help_but = tki.Button(self, text = help_face, command=self._show_help,
                                   width=5, takefocus=0)   # don't take part in tab-focus
        self.help_but.grid(row=0, column=2)

        # initialise it, which triggers the trace, _change and validation
        self.var.set(str(data))

    def _show_help(self):
        tkm.showinfo('conversion information', '{}\n{}'.format(self.conv_help, self.err))

    def _changed(self, *args):
        ent_val = self.var.get()
        try:
            self.value = self.conv(ent_val)
            self.entry.config(bg='white')
            self.err = ''
            self.valid = True
        except ValueError as err:
            self.value = None
            self.entry.config(bg='orange')
            self.err = err
            self.valid = False
        self.update()

class GUI_inputs(tki.LabelFrame):
    """ A GUI data input convenience class"""
    def __init__(self, parent, text='Input Widget', execute=None):
        """ initialise with text for the LabelFrame
            if execute is defined, create an 'executeute' button in the frame
            greyed out until all entries are valid
        """
        tki.LabelFrame.__init__(self, master=parent, text=text)

        # we have a list of entries
        self.entries = []    # the data entry widgets
        self.keys = []      # the keys, both lists will stay in the same order
        self.row = 0

        # if there's a execute supplied, put up a button for it, on the last row
        self.execute_func = execute
        if execute:
            self.execute_but = tki.Button(self, text='execute',
                                           command=self.execute_func,
                                           state=tki.DISABLED)
            self.execute_but.grid(row=99, column=0)    

    def add(self, key, disp_name='', data='', conv=None):
        """ add a new line to the input widget

        key         required    key for the entry, must be unique
        other arguments follow from MyLabelEntry() usage above                                     
        """

        if key in self.keys:
            raise ValueError('duplicate key name >>>{}<<<'.format(key))

        if not disp_name:
            disp_name = str(key)

        mle = MyLabelEntry(self, disp_name, data, conv, self.update)
        mle.grid(row=self.row)
        self.row += 1
        self.entries.append(mle)
        self.keys.append(key)

    def update(self):
        """ called when something has changed"""
        # only need to worry about this when there's a execute button to grey-out
        if self.execute_func:
            # get the valid properties of each entry
            valids = [x.valid for x in self.entries]
            if all(valids):
                self.execute_but.config(state=tki.NORMAL)
            else:
                self.execute_but.config(state=tki.DISABLED)            

    def get(self):
        """ return a dict of the results"""
        output = dict(zip(self.keys, [x.value for x in self.entries]))
        return output

def intfloat(x):
    """[if] a float accepted, truncated to an integer for return"""
    return int(float(x))

def int16(x):
    """[16] a base16 (hexadecimal) integer"""
    return int(x, base=16)

def float_pair(x):
    """[f,f] Two floats seperated by a comma
    all three elements are required
    [end_help]

    example non-trivial conversion function
    not all of docstring intended to be displayed as help
    throw two different types of ValueError, one from split, one from float
    return a list of the values

    """
    fields = x.split(',')
    if len(fields) != 2:
        raise ValueError('need two fields seperated by one comma')
    output = []
    for field in fields:  # no need to try:, ValueError will burn through
        output.append(float(field))
    return output    

if __name__ == '__main__':

    def execute_func():
        print('executing with')
        print( ml.get(), basic.get())

    def cryptic_conv(x):
        # no docstring for this conversion function
        return int(x)

    root = tki.Tk()

    basic = GUI_inputs(root, 'basic')
    basic.pack()
    basic.add('key 1')
    basic.add('key 2')

    ml = GUI_inputs(root, 'more flexible', execute=execute_func)
    ml.pack()
    ml.add('we')
    ml.add('we1', conv=int)
    ml.add('we2', 'disp4we2', data=999)
    ml.add('pair', 'f_pair','',float_pair)
    ml.add('cryp', 'no doc string', 6, cryptic_conv)

    root.mainloop()
11 голосов | спросил Neil_UK 4 J0000006Europe/Moscow 2014, 03:23:39

2 ответа


6

Я запустил ваш код и немного перепутал его, и он работает очень хорошо. У вас также есть довольно крутая идея, что мне, возможно, придется красть в следующий раз, когда я делаю TKinter GUI.

Что касается высокоуровневого дизайна, я согласен с вашим решением о том, чтобы каждая строка ввода была его собственным объектом, о чем гораздо проще думать, чем о другом подходе, описанном вами, с помощью GUI_Input, непосредственно управляющий объектами TKinter. Вы добавили достаточно поведения, чтобы эти MyLabelEntry вещи не были такими же, как и их составные объекты, поэтому имеет смысл сделать новый класс, который имеет дело с этим.

Мне действительно не нравится ваш подход к списку. Мне кажется, что вы делаете параллельные массивы, что очень часто используется в C, поскольку у него нет реального типа словаря. Вы полагаетесь на ключи и значения (записи), находящиеся в одной позиции в двух разных списках. Обычно я предпочитаю словари. Если порядок важен, вы можете использовать класс Collections.OrderedDict, если вы используете Python 2.7 или выше. (Это выглядит из вашего кода TKinter, как вы используете Python 3.) Для меня упорядоченный словарь чувствует себя более чистым и более Pythonic, и нет никакой опасности (хотя и удаленной) ваших ключей и значений, выходящих из синхронизации. (Я говорю это как человек, который провел год, кодируя все мои словари как параллельные списки из-за привычек, которые я разработал после сдачи C ++ в университете.)

Как я уже упоминал в своем комментарии, у меня есть некоторые проблемы с тем, как вы назвали вещи. Для PEP-8 стандартом является использование CamelCase для имен классов и snake_case для функций и методов. Я понимаю, что GUIInputs ужасен; в Java они пишут GuiInputs, но PEP-8 предпочитает, чтобы вы полностью капитализировали аббревиатуры, полностью заглавные. Чтобы обойти это, я бы рекомендовал просто изменить имя. Если я правильно читаю код, GUI_inputs является контейнером для MyLabelEntry, так что вы могли бы назвать это чем-то вроде LabelEntryContainer.

Я также рекомендую изменить MyLabelEntry. Это мое домашнее животное, но я ненавижу его, когда люди называют вещи «моими». Кажется, это перлизм; на этом языке вы объявляете переменные с помощью my, как Javascript и Scala объявляют их с помощью var. Мне не нравится «мой», потому что он делает имена длиннее, не добавляя больше информации. Действительно, что означает MyLabelEntry, что LabelEntry не делает? Насколько я могу судить, две дополнительные буквы.

Наконец, я бы рекомендовал более чистый способ управлять функциями преобразования внутри MyLabelEntry. То, что у вас сейчас есть, - это, в основном, гигантский оператор switch, который несколько не поддается расширению. Функции в Python являются первоклассными, поэтому вы можете вместо этого хранить свои функции преобразования в словаре. Я попробовал это, и похоже, что функции даже хешируются, поэтому вы можете использовать их в качестве словарных клавиш, если это то, что вам нужно. Значения словаря могут быть кортежи или словари, которые задают параметры переменных в каждом случае. Дайте мне знать, если вам нужно посмотреть пример, и я отредактирую для добавления.

Этот подход также разделил бы много кода в вашей функции __init__, работающей с conv. Это хорошо, потому что обычно считается лучшим стилем для сохранения функции __init__. Если вам нужно сделать что-то сложное, вы можете написать помощники (которые были бы приватными на Java) и вызвать их из __init__.

Он может использовать небольшую очистку, но у вас есть довольно хорошо продуманный код здесь, и хорошая идея для вспомогательного класса ускорить развитие GUI.

ответил tsleyson 16 TueEurope/Moscow2014-12-16T02:15:48+03:00Europe/Moscow12bEurope/MoscowTue, 16 Dec 2014 02:15:48 +0300 2014, 02:15:48
2

Добавление следующего в начало - все, что вам нужно, чтобы этот код был совместим с Bacwards с Python 2.

import sys
if sys.version == 2:
    import Tkinter as tki
    import tkMessageBox as tkm
else:
    import tkinter as tki
    import tkinter.messagebox as tkm
ответил Caridorc 10 WedEurope/Moscow2014-12-10T00:06:53+03:00Europe/Moscow12bEurope/MoscowWed, 10 Dec 2014 00:06:53 +0300 2014, 00:06:53

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132