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

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

Python Magazine

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

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

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

  1. Скрипт для создания пользовательского словаря.
  2. GUI интерфейс и набор CRUD операций для добавления, редактирования и удаления записей в словаре.
  3. GUI интерфейс и скрипт для проверки правильности сопоставления иностранных слов и значений, выведенных в случайном порядке.
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

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 для запоминания и проверки значений слов:

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()

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

Report Page