Tkinter. ООП в приложении

Объектно-ориентированное программирование – очень удобный тип программирования, который позволяет работать с данными, как с объектами. Это позволяет с легкостью модифицировать определенные объекты кода, осуществлять откат, если возникают какие-то ошибки. Такие программы легко расширяются дополнительными функциями, если разработчик предусматривает такую возможность. И в целом, объектно-ориентированное программирование позволяет существенно упростить разработку программы.

Сегодня рассмотрим, как осуществляется работа с объектами в Tkinter. Ведь все мы понимаем, что каждый класс – это объект, верно?

Использование классов для структурирования данных

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

Какая информация туда включается?

  1. Имя и фамилия. Очевидно, что здесь должно храниться какое-то значение.
  2. Адрес электронной почты. Допустим, у нас он следующий: alexx345@gmail.com
  3. Номер телефона. Он также указывается в определенном формате. Например, таком: (345) 6789012

Это абстракция – набор обобщенных характеристик, которые и отличают элемент определенного класса. В нашем случае таковым будет Contact.

Сперва необходимо определить служебных функций, используемых для валидации обязательных полей. 

Также важно определить те из них, которые выполняют проверку на соответствие полей ввода электронной почты и номера телефона тем форматам, которые указаны выше.

В результате, получится следующий код:

def required(value, message):

    if not value:

        raise ValueError(message)

    return value




def matches(value, regex, message):

    if value and not regex.match(value):

        raise ValueError(message)

    return value

Затем необходимо определить наш класс Contact, а также метод __init__, в котором укажем все параметры полей, в которые будет вводиться информация. Также скомпилированные регулярные выражения необходимо сохранить, так как они будут применяться для всех элементов во время проверки полей.

import re




class Contact(object):

    email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+")

    phone_regex = re.compile(r"\([0-9]{3}\)\s[0-9]{7}")




    def __init__(self, last_name, first_name, email, phone):

        self.last_name = last_name

        self.first_name = first_name

        self.email = email

        self.phone = phone

Как видим из этого фрагмента кода, мы импортировали библиотеку re. Тем не менее, недостаточно лишь этого определения, чтобы была возможна проверка полей. Чтобы реализовать это, необходимо использовать декоратор @property. Он позволяет работать с внутренними атрибутами.

    @property

    def last_name(self):

        return self._last_name




    @last_name.setter

    def last_name(self, value):

        self._last_name = required(value, "Фамилия обязательна")

Аналогичный прием касается и first_name, так как и это поле – обязательное. Подход аналогичный для атрибутов email и phone. Также они задействуют функцию matches с подходящими регулярными выражениями.

    @property

    def email(self):

        return self._email




    @email.setter

    def email(self, value):

        self._email = matches(value, self.email_regex, "Invalid email format")

Далее этот готовый код нужно будет использовать. Для этого необходимо сохранить его в отдельном файле, и потом на него ссылаться.

Как мы уже указывали ранее, с помощью механизма property мы запускаем вызовы функции, работая с атрибутами объекта. 

В данном примере они используют внутренние атрибуты, содержащие в начале названия нижнее подчеркивание. 

contact.first_name = «John»  # Сохраняется «John» в contact._first_name

print(contact.first_name)       # Читается «John» из contact._first_name

contact.last_name = «»         # ValueError вызвано функцией проверки данных

Как правило, дескриптор property применяется вместе с синтаксисом @decorated. Для декорируемых функций необходимо использовать одинаковое имя. Важно помнить это.

    @property

    def last_name(self):

        # ...




    @last_name.setter

    def last_name(self, value):

        # ...

На первый взгляд, полная реализация класса Contact может показаться несколько избыточной, с большим количеством шаблонного кода. Ведь атрибуты необходимо сначала присвоить в методе __init__, а затем задать геттеры и сеттеры. Чтобы обойти эту проблему, используется функция namedtuple, которая находится в стандартной библиотеке. Она позволяет генерировать простые подклассы кортежей с именованными полями.

from collections import namedtuple




Contact = namedtuple("Contact", ["last_name", "first_name",

                                       "email", "phone"])

Тем не менее, по-прежнему необходимо реализовать обходной путь проверки полей. Для этого используется Python Package Index, а именно, пакет attrs. Чтобы его инсталлировать, необходимо воспользоваться командной строкой, указав там команду pip и подкомманду install.

pip install attrs

Затем уже все свойства заменяются на attr.ib. Это также дает возможность задавать функцию обратного вызова validator, которая принимает объект, редактируемый атрибут и значение, которое необходимо использовать.

Таким образом, можно уменьшить количество строк, необходимых для объявления класса Contact, в два раза.

import re

import attr




def required(message):

    def func(self, attr, val):

        if not val: raise ValueError(message)

    return func




def match(pattern, message):

    regex = re.compile(pattern)

    def func(self, attr, val):

        if val and not regex.match(val):

            raise ValueError(message)

    return func




@attr.s

class Contact(object):

    last_name = attr.ib(validator=required("Фамилия обязательна"))

    first_name = attr.ib(validator=required("Имя обезательно"))

    email = attr.ib(validator=match(r"[^@]+@[^@]+\.[^@]+",

                                    "Ошибка в поле email"))

    phone = attr.ib(validator=match(r"\([0-9]{3}\)\s[0-9]{7}",

                                    "Ошибка в поле phone"))

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

Более подробную информацию о пакете attrs можно найти здесь.

Как создавать виджеты для отображения информации?

Чтобы создавать приложения было комфортно, необходимо разбивать его на отдельные классы. Тяжело сделать качественно приложение, если весь код предусмотрен лишь в одном классе. А вот при модульной организации приложения появляется возможность создавать отдельный элемент для каждой задачи. 

В прошлом примере мы создали класс Contact. Его необходимо импортировать.

import tkinter as tk

import tkinter.messagebox as mb




from with_attr import Contact

Необходимо удостовериться в том, что файл with_attr.py расположен в той же папке, что и этот код. В другом же случае эта инструкция выдаст ошибку ImportError.

Давайте создадим список контактов с поддержкой функции скроллинга. Чтобы каждый элемент имел строчное представление, необходимо использовать такой код, чтобы отобразить имя и фамилию каждого контакта.

class ContactList(tk.Frame):

    def __init__(self, master, **kwargs):

        super().__init__(master)

        self.lb = tk.Listbox(self, **kwargs)

        scroll = tk.Scrollbar(self, command=self.lb.yview)




        self.lb.config(yscrollcommand=scroll.set)

        scroll.pack(side=tk.RIGHT, fill=tk.Y)

        self.lb.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)




    def insert(self, contact, index=tk.END):

        text = "{}, {}".format(contact.last_name, contact.first_name)

        self.lb.insert(index, text)




    def delete(self, index):

        self.lb.delete(index, index)




    def update(self, contact, index):

        self.delete(index)

        self.insert(contact, index)




    def bind_doble_click(self, callback):

        handler = lambda _: callback(self.lb.curselection()[0])

        self.lb.bind("", handler)

Чтобы была возможность просматривать и модифицировать контакты, необходимо создать отдельную форму. Для начала попробуем взять в качестве базового класса LabelFrame с Label и Entry для каждого поля.

class ContactForm(tk.LabelFrame):

    fields = ("Фамилия", "Имя", "Email", "Телефон")




    def __init__(self, master, **kwargs):

        super().__init__(master, text="Contact", padx=10, pady=10, **kwargs)

        self.frame = tk.Frame(self)

        self.entries = list(map(self.create_field, enumerate(self.fields)))

        self.frame.pack()




    def create_field(self, field):

        position, text = field

        label = tk.Label(self.frame, text=text)

        entry = tk.Entry(self.frame, width=25)

        label.grid(row=position, column=0, pady=5)

        entry.grid(row=position, column=1, pady=5)

        return entry




    def load_details(self, contact):

        values = (contact.last_name, contact.first_name,

                  contact.email, contact.phone)

        for entry, value in zip(self.entries, values):

            entry.delete(0, tk.END)

            entry.insert(0, value)




    def get_details(self):

        values = [e.get() for e in self.entries]

        try:

            return Contact(*values)

        except ValueError as e:

            mb.showerror("Ошибка валидации", str(e), parent=self)




    def clear(self):

        for entry in self.entries:

            entry.delete(0, tk.END)

Наш виджет предоставляет возможность добавить функцию обратного вызова для клика левой кнопкой мыши два раза. Также с его помощью можно получить индекс клика в качестве аргумента функции. Все это позволяет спрятать особенности внутреннего класса Listbox.

    def bind_doble_click(self, callback):

        handler = lambda _: callback(self.lb.curselection()[0])

        self.lb.bind("<Double-Button-1&rt;", handler)

ОфисГуру
Adblock
detector