Python, Tkinter и SQL: разрабатываем приложение для создания словаря и запоминания иностранных слов.

Введение

Изучаем Tkinter и основные SQL-команды в ходе разработки программы WordMatch с графическим интерфейсом и CRUD-модулем для удобного создания и редактирования пользовательских словарей.

Обзор проекта

Приложение WordMatch включает в себя три модуля, которые могут работать и вместе, и по отдельности:

  1. Скрипт для создания пользовательского словаря.
  2. GUI интерфейс и набор CRUD операций для добавления, редактирования и удаления записей в словаре.
  3. GUI интерфейс и скрипт для проверки правильности сопоставления иностранных слов и значений, выведенных в случайном порядке.
WordMatch состоит из трех независимых скриптов
WordMatch состоит из трех независимых скриптов

Что мы изучим

  1. Как создавать базы данных, выполнять CRUD операции и запросы на языке SQL.
  2. Как обрабатывать события в элементах Listbox.
  3. Как назначить действия основным кнопкам программы и кнопке закрытия окна.

Скрипт для создания словаря

Словарь представляет собой базу данных SQLite, которая поставляется с Python по умолчанию. Для создания новой базы не придется устанавливать никакие дополнительные модули. Однако при желании можно установить набор дополнительных инструментов для работы с SQLite и один из визуальных браузеров-редакторов:

  1. SQLiteStudio
  2. DBeaver
  3. DB Browser for SQLite

Структура таблицы словаря dictionary задается в sql_create_dictionary_table скрипта create_new_db.py:

  • id – порядковый номер записи (целое число);
  • word– иностранное слово (текстовое поле);
  • meaning – значение слова на русском языке (текстовое поле).

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

        unable to open database file
Ошибка: не удалось подключиться к базе.

    

Если же путь вообще не указан, файл базы данных будет создан в текущей рабочей директории – в Windows это C:\Users\User.

При подключении к несуществующей базе SQLite создает файл базы автоматически, но только при условии, что указанный путь существует. Ниже приведен полный код скрипта для создания базы данных словаря. При этом папка Dictionary в поддиректории Users была создана заранее, а файл dictionary.db в ней был сгенерирован скриптом автоматически:create_new_db.py

        import sqlite3
from sqlite3 import Error

def create_connection(db_file):
    conn = None
    try:
        conn = sqlite3.connect(db_file)
        return conn
    except Error as e:
        print(e)

    return conn

def create_table(conn, create_table_sql):
    try:
        c = conn.cursor()
        c.execute(create_table_sql)
    except Error as e:
        print(e)

def main():
    database = r"dictionary_my.db"
    # описание столбцов словаря - id номер, слово и значение
    sql_create_dictionary_table = """ CREATE TABLE IF NOT EXISTS dictionary (
                                        id integer PRIMARY KEY,
                                        word text,
                                        meaning text
                                    ); """


    # подключение к базе
    conn = create_connection(database)

    # создание таблицы dictionary
    if conn is not None:
        create_table(conn, sql_create_dictionary_table)
    else:
        print("Ошибка: не удалось подключиться к базе.")


if __name__ == '__main__':
    main()
    

    

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

Интересно, перейти к каналу

GUI интерфейс и скрипт для набора CRUD операций

Графический интерфейс программы включает стандартные элементы Tkinter и несколько виджетов модуля Ttk. Для позиционирования элементов на поверхности окна в Tkinter есть целых три метода – pack()place() и grid(). Мы воспользуемся последним, поскольку он предусматривает максимальную точность размещения. При использовании grid() все пространство окна делится на ряды row и столбцы column. Для каждого элемента нужно указать ряд и столбец, на пересечении которых он размещается:

        (row = 2, column = 0)
    

Еще можно указать ширину элемента, если нужно, чтобы он соответствовал ширине нескольких столбцов:

        columnspan = 2
    
Визуальный интерфейс для CRUD
Визуальный интерфейс для CRUD

SQL-запросы и команды

Первый запрос, который нам потребуется для извлечения из базы всех слов, выглядит так:

        'SELECT * FROM dictionary ORDER BY word DESC'
    

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

В функции add_word() используется команда для вставки новой записи:

        'INSERT INTO dictionary VALUES(NULL, ?, ?)'
    

Вопросительными знаками обозначаются параметры, которые передаются (из соответствующих полей формы) на следующей строке:

        parameters = (self.word.get(), self.meaning.get())
    
Сообщение об успешном добавлении нового слова
Сообщение об успешном добавлении нового слова

Для удаления слова необходимо выделить соответствующую строку. Слово извлекается из выделенной строки:

        word = self.tree.item(self.tree.selection())['text']
    

И передается в качестве параметра с командой удаления:

        query = 'DELETE FROM dictionary WHERE word = ?'
self.run_query(query, (word, ))
    

В функции редактирования существующей записи мы реализуем предварительное заполнение поля формы старыми значениями – оригиналом слова и его переводом:

        value = word
value = old_meaning

    
Предварительное заполнение полей в окне редактирования
Предварительное заполнение полей в окне редактирования

Это нужно для того, чтобы при сохранении записи не сохранилось пустое поле вместо предыдущего слова или значения, если одно из них не нужно было редактировать и пользователь не ввел слово (значение) вручную. А еще это упрощает исправление опечаток.

Фактическое обновление существующей записи производится командой со следующими параметрами:

        query = 'UPDATE dictionary SET word = ?, meaning = ? WHERE word = ? AND meaning = ?'
parameters = (new_word, new_meaning, word, old_meaning)
    

Вот полный код для CRUD скрипта и его интерфейса:

        from tkinter import ttk
from tkinter import *
import sqlite3

class Dictionary:
    db_name = 'dictionary.db'

    def __init__(self, window):

        self.wind = window
        self.wind.title('Редактирование словаря')

        # создание элементов для ввода слов и значений
        frame = LabelFrame(self.wind, text = 'Введите новое слово')
        frame.grid(row = 0, column = 0, columnspan = 3, pady = 20)
        Label(frame, text = 'Слово: ').grid(row = 1, column = 0)
        self.word = Entry(frame)
        self.word.focus()
        self.word.grid(row = 1, column = 1)
        Label(frame, text = 'Значение: ').grid(row = 2, column = 0)
        self.meaning = Entry(frame)
        self.meaning.grid(row = 2, column = 1)
        ttk.Button(frame, text = 'Сохранить', command = self.add_word).grid(row = 3, columnspan = 2, sticky = W + E)
        self.message = Label(text = '', fg = 'green')
        self.message.grid(row = 3, column = 0, columnspan = 2, sticky = W + E)
        # таблица слов и значений
        self.tree = ttk.Treeview(height = 10, columns = 2)
        self.tree.grid(row = 4, column = 0, columnspan = 2)
        self.tree.heading('#0', text = 'Слово', anchor = CENTER)
        self.tree.heading('#1', text = 'Значение', anchor = CENTER)

        # кнопки редактирования записей
        ttk.Button(text = 'Удалить', command = self.delete_word).grid(row = 5, column = 0, sticky = W + E)
        ttk.Button(text = 'Изменить', command = self.edit_word).grid(row = 5, column = 1, sticky = W + E)

        # заполнение таблицы
        self.get_words()

    # подключение и запрос к базе
    def run_query(self, query, parameters = ()):
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            result = cursor.execute(query, parameters)
            conn.commit()
        return result

    # заполнение таблицы словами и их значениями
    def get_words(self):
        records = self.tree.get_children()
        for element in records:
            self.tree.delete(element)
        query = 'SELECT * FROM dictionary ORDER BY word DESC'
        db_rows = self.run_query(query)
        for row in db_rows:
            self.tree.insert('', 0, text = row[1], values = row[2])

    # валидация ввода
    def validation(self):
        return len(self.word.get()) != 0 and len(self.meaning.get()) != 0
    # добавление нового слова
    def add_word(self):
        if self.validation():
            query = 'INSERT INTO dictionary VALUES(NULL, ?, ?)'
            parameters =  (self.word.get(), self.meaning.get())
            self.run_query(query, parameters)
            self.message['text'] = 'слово {} добавлено в словарь'.format(self.word.get())
            self.word.delete(0, END)
            self.meaning.delete(0, END)
        else:
            self.message['text'] = 'введите слово и значение'
        self.get_words()
    # удаление слова 
    def delete_word(self):
        self.message['text'] = ''
        try:
            self.tree.item(self.tree.selection())['text'][0]
        except IndexError as e:
            self.message['text'] = 'Выберите слово, которое нужно удалить'
            return
        self.message['text'] = ''
        word = self.tree.item(self.tree.selection())['text']
        query = 'DELETE FROM dictionary WHERE word = ?'
        self.run_query(query, (word, ))
        self.message['text'] = 'Слово {} успешно удалено'.format(word)
        self.get_words()
    # рeдактирование слова и/или значения
    def edit_word(self):
        self.message['text'] = ''
        try:
            self.tree.item(self.tree.selection())['values'][0]
        except IndexError as e:
            self.message['text'] = 'Выберите слово для изменения'
            return
        word = self.tree.item(self.tree.selection())['text']
        old_meaning = self.tree.item(self.tree.selection())['values'][0]
        self.edit_wind = Toplevel()
        self.edit_wind.title = 'Изменить слово'

        Label(self.edit_wind, text = 'Прежнее слово:').grid(row = 0, column = 1)
        Entry(self.edit_wind, textvariable = StringVar(self.edit_wind, value = word), state = 'readonly').grid(row = 0, column = 2)
        
        Label(self.edit_wind, text = 'Новое слово:').grid(row = 1, column = 1)
        # предзаполнение поля
        new_word = Entry(self.edit_wind, textvariable = StringVar(self.edit_wind, value = word))
        new_word.grid(row = 1, column = 2)


        Label(self.edit_wind, text = 'Прежнее значение:').grid(row = 2, column = 1)
        Entry(self.edit_wind, textvariable = StringVar(self.edit_wind, value = old_meaning), state = 'readonly').grid(row = 2, column = 2)
 
        Label(self.edit_wind, text = 'Новое значение:').grid(row = 3, column = 1)
        # предзаполнение поля
        new_meaning= Entry(self.edit_wind, textvariable = StringVar(self.edit_wind, value = old_meaning))
        new_meaning.grid(row = 3, column = 2)

        Button(self.edit_wind, text = 'Изменить', command = lambda: self.edit_records(new_word.get(), word, new_meaning.get(), old_meaning)).grid(row = 4, column = 2, sticky = W)
        self.edit_wind.mainloop()
    # внесение изменений в базу
    def edit_records(self, new_word, word, new_meaning, old_meaning):
        query = 'UPDATE dictionary SET word = ?, meaning = ? WHERE word = ? AND meaning = ?'
        parameters = (new_word, new_meaning, word, old_meaning)
        self.run_query(query, parameters)
        self.edit_wind.destroy()
        self.message['text'] = 'слово {} успешно изменено'.format(word)
        self.get_words()

if __name__ == '__main__':
    window = Tk()
    application = Dictionary(window)
    window.mainloop()


    

Модуль для запоминания слов и проверки значений

Английские слова и их значения загружаются в два элемента Listbox. Для перемешивания слов и значений в случайном порядке используется метод shuffle из модуля random. Для обработки событий (кликов) по спискам Listbox мы напишем две функции – callback_left и callback_right. Чтобы связать функции с Listbox, нужно воспользоваться методом bind:

        self.right.bind("<<ListboxSelect>>", self.callback_right)
self.left.bind("<<ListboxSelect>>", self.callback_left)

    
Слова и значения выводятся в случайном порядке
Слова и значения выводятся в случайном порядке

Функция callback_left отслеживает клики по английским словам в левом элементе Listbox. Когда пользователь кликает по слову, функция посылает запрос в базу:

        'SELECT * from dictionary WHERE word = ?'
    

Результат запроса – отдельная запись:

        record = cursor.fetchone()
    

Второй элемент записи record[2] является значением слова, которое передается в функцию callback_right.

Функция callback_right обрабатывает клики по значениям слов в правом списке Listbox. Когда пользователь кликает по значению, функция сравнивает его со значением, полученным из callback_left. Если они совпадают – ответ является верным, и английское слово вместе с соответствующим значением удаляются из левого и правого списков:

        if click == self.trans:
    self.right.delete(ANCHOR)
    self.left.delete(ANCHOR)
    

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

В GUI модуля для заучивания слов доступны две кнопки: Редактировать для вызова скрипта и визуального интерфейса редактирования словаря, а также Начать сначала для перезагрузки слов и значений. Чтобы вызвать скрипт редактирования словаря, достаточно написать простейшую функцию:

        def run_edit(self):
    os.system('edit_dictionary.py')
    

Назначение команд для кнопок выглядит так:

        ttk.Button(text="Начать сначала", command=self.restart_program).grid(row = 4, column = 1, sticky = W + E)
ttk.Button(text="Редактировать", command=self.run_edit).grid(row = 4, column = 0, sticky = W + E)
    
Редактировать словарь можно прямо из модуля запоминания слов
Редактировать словарь можно прямо из модуля запоминания слов

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

Подтверждение выхода
Подтверждение выхода

Для этого нужно задать новый протокол:

        self.wind.protocol("WM_DELETE_WINDOW", self.on_exit)
    

И добавить функцию:

            def on_exit(self):
        if messagebox.askyesno("Выйти", "Закрыть программу?"):
            self.wind.destroy()
    

Это полный код модуля word_match.py для запоминания и проверки значений слов:word_match.py

        from tkinter import ttk
from tkinter import *
import random, os
import sqlite3

class Match:
    db_name = 'dictionary.db'

    def __init__(self, window):

        self.wind = window
        self.wind.title('Учим слова')
        self.eng, self.trans = str(), str()
        self.message = Label(text = '', fg = 'red')
        self.message.grid(row = 1, column = 0, columnspan = 2, sticky = W + E)
        # правая и левая колонки
        self.left = Listbox(height = 12, exportselection=False, activestyle='none')
        self.left.grid(row = 2, column = 0)
        self.right = Listbox(height = 12, activestyle='none')
        self.right.grid(row = 2, column = 1)
        self.right.bind("<<ListboxSelect>>", self.callback_right)
        self.left.bind("<<ListboxSelect>>", self.callback_left)
        # назначение команд кнопкам программы и х-кнопке окна
        ttk.Button(text="Начать сначала", command=self.restart_program).grid(row = 4, column = 1, sticky = W + E)
        ttk.Button(text="Редактировать", command=self.run_edit).grid(row = 4, column = 0, sticky = W + E)
        self.wind.protocol("WM_DELETE_WINDOW", self.on_exit)
        # заполняем колонки словами  
        self.get_words()
    #  закрытие программы по клику на кнопке х
    def on_exit(self):
        if messagebox.askyesno("Выйти", "Закрыть программу?"):
            self.wind.destroy()
    #  подключение к базе и передача запроса
    def run_query(self, query, parameters = ()):
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            result = cursor.execute(query, parameters)
            conn.commit()
        return result
 
    # запрос на извлечение всех существующих записей из базы в алфавитном порядке
    def get_words(self):
        query = 'SELECT * FROM dictionary ORDER BY word DESC'
        db_rows = self.run_query(query)
        # формирование словаря из перемешанных в случайном порядке слов и их значений
        lst_left, lst_right = [], []
        for row in db_rows:
            lst_left.append(row[1])
            lst_right.append(row[2])
        random.shuffle(lst_left)
        random.shuffle(lst_right)
        dic = dict(zip(lst_left, lst_right))
        # заполнение правой и левой колонок
        for k, v in dic.items():
            self.left.insert(END, k)
            self.right.insert(END, v)
    # обработка клика по словам в левой колонке
    def callback_left(self, event):
        self.message['text'] = ''
        if not event.widget.curselection():
            return
        # извлечение из базы значения выделенного слова
        w = event.widget
        idx = int(w.curselection()[0])
        self.eng = w.get(idx)
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            sqlite_select_query = 'SELECT * from dictionary WHERE word = ?'
            cursor.execute(sqlite_select_query, (self.eng,))
            record = cursor.fetchone()
            self.trans = record[2]
   
    # обработка клика в правой колонке  
    def callback_right(self, event1):
        self.message['text'] = ''
        if not event1.widget.curselection():
            return
        
        w = event1.widget
        idx = int(w.curselection()[0])
        click = w.get(idx)
        # если выбранное слово является правильным переводом, удаляем и оригинал, и значение
        if click == self.trans:
            self.right.delete(ANCHOR)
            self.left.delete(ANCHOR)
        # сообщаем о неверном значении
        else:
            self.message['text'] = 'Неправильно'
            self.right.selection_clear(0, END)
    # загружаем окно и скрипт редактирования словаря        
    def run_edit(self):
        os.system('edit_dictionary.py')
    # перезапуск программы
    def restart_program(self):
        self.message['text'] = ''
        self.left.delete(0, END)
        self.right.delete(0, END)
        self.get_words()

if __name__ == '__main__':
    window = Tk()
    window.geometry('250x245+350+200')
    application = Match(window)
    window.mainloop()

Готовый проект доступен в репозитории.
источник

Ответить